import {InputElement} from "./InputElement"; import Loc from "../../Models/Loc"; import {UIEventSource} from "../../Logic/UIEventSource"; import Minimap from "../Base/Minimap"; import BaseLayer from "../../Models/BaseLayer"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; import State from "../../State"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; import {GeoOperations} from "../../Logic/GeoOperations"; import ShowDataLayer from "../ShowDataLayer"; import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import * as L from "leaflet"; export default class LocationInput extends InputElement { IsSelected: UIEventSource = new UIEventSource(false); private _centerLocation: UIEventSource; private readonly mapBackground: UIEventSource; private readonly _snapTo: UIEventSource<{ feature: any }[]> private readonly _value: UIEventSource private readonly _snappedPoint: UIEventSource private readonly _maxSnapDistance: number private readonly _snappedPointTags: any; public readonly snappedOnto: UIEventSource = new UIEventSource(undefined) constructor(options: { mapBackground?: UIEventSource, snapTo?: UIEventSource<{ feature: any }[]>, maxSnapDistance?: number, snappedPointTags?: any, requiresSnapping?: boolean, centerLocation: UIEventSource, }) { super(); this._snapTo = options.snapTo?.map(features => features?.filter(feat => feat.feature.geometry.type !== "Point")) this._maxSnapDistance = options.maxSnapDistance this._centerLocation = options.centerLocation; this._snappedPointTags = options.snappedPointTags if (this._snapTo === undefined) { this._value = this._centerLocation; } else { const self = this; let matching_layer: UIEventSource if (self._snappedPointTags !== undefined) { matching_layer = State.state.layoutToUse.map(layout => { for (const layer of layout.layers) { if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) { return layer.id } } console.error("No matching layer found for tags ", self._snappedPointTags) return "matchpoint" }) } else { matching_layer = new UIEventSource("matchpoint") } this._snappedPoint = options.centerLocation.map(loc => { if (loc === undefined) { return undefined; } // We reproject the location onto every 'snap-to-feature' and select the closest let min = undefined; let matchedWay = undefined; for (const feature of self._snapTo.data) { const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [loc.lon, loc.lat]) if (min === undefined) { min = nearestPointOnLine matchedWay = feature.feature; continue; } if (min.properties.dist > nearestPointOnLine.properties.dist) { min = nearestPointOnLine matchedWay = feature.feature; } } if (min.properties.dist * 1000 > self._maxSnapDistance) { if (options.requiresSnapping) { return undefined } else { return { "type": "Feature", "_matching_layer_id": matching_layer.data, "properties": options.snappedPointTags ?? min.properties, "geometry": {"type": "Point", "coordinates": [loc.lon, loc.lat]} } } } min._matching_layer_id = matching_layer?.data ?? "matchpoint" min.properties = options.snappedPointTags ?? min.properties self.snappedOnto.setData(matchedWay) return min }, [this._snapTo]) this._value = this._snappedPoint.map(f => { const [lon, lat] = f.geometry.coordinates; return { lon: lon, lat: lat, zoom: undefined } }) } this.mapBackground = options.mapBackground ?? State.state.backgroundLayer ?? new UIEventSource(AvailableBaseLayers.osmCarto) this.SetClass("block h-full") } GetValue(): UIEventSource { return this._value; } IsValid(t: Loc): boolean { return t !== undefined; } protected InnerConstructElement(): HTMLElement { try { const clickLocation = new UIEventSource(undefined); const map = new Minimap( { location: this._centerLocation, background: this.mapBackground, attribution: this.mapBackground !== State.state.backgroundLayer, lastClickLocation: clickLocation } ) clickLocation.addCallbackAndRunD(location => this._centerLocation.setData(location)) map.leafletMap.addCallbackAndRunD(leaflet => { const bounds = leaflet.getBounds() leaflet.setMaxBounds(bounds.pad(0.15)) const data = { type: "FeatureCollection", features: [{ "type": "Feature", "geometry": { "type": "LineString", "coordinates": [ [ bounds.getEast(), bounds.getNorth() ], [ bounds.getWest(), bounds.getNorth() ], [ bounds.getWest(), bounds.getSouth() ], [ bounds.getEast(), bounds.getSouth() ], [ bounds.getEast(), bounds.getNorth() ] ] } }] } // @ts-ignore L.geoJSON(data, { style: { color: "#f00", weight: 2, opacity: 0.4 } }).addTo(leaflet) }) if (this._snapTo !== undefined) { new ShowDataLayer(this._snapTo, map.leafletMap, State.state.layoutToUse, false, false) const matchPoint = this._snappedPoint.map(loc => { if (loc === undefined) { return [] } return [{feature: loc}]; }) if (this._snapTo) { let layout = LocationInput.matchLayout if (this._snappedPointTags !== undefined) { layout = State.state.layoutToUse } new ShowDataLayer( matchPoint, map.leafletMap, layout, false, false ) } } this.mapBackground.map(layer => { const leaflet = map.leafletMap.data if (leaflet === undefined || layer === undefined) { return; } leaflet.setMaxZoom(layer.max_zoom) leaflet.setMinZoom(layer.max_zoom - 2) leaflet.setZoom(layer.max_zoom - 1) }, [map.leafletMap]) return new Combine([ new Combine([ Svg.move_arrows_ui() .SetClass("block relative pointer-events-none") .SetStyle("left: -2.5rem; top: -2.5rem; width: 5rem; height: 5rem") ]).SetClass("block w-0 h-0 z-10 relative") .SetStyle("background: rgba(255, 128, 128, 0.21); left: 50%; top: 50%"), map .SetClass("z-0 relative block w-full h-full bg-gray-100") ]).ConstructElement(); } catch (e) { console.error("Could not generate LocationInputElement:", e) return undefined; } } private static readonly matchLayout = new UIEventSource(new LayoutConfig({ description: "Matchpoint style", icon: "./assets/svg/crosshair-empty.svg", id: "matchpoint", language: ["en"], layers: [{ id: "matchpoint", source: { osmTags: {and: []} }, icon: "./assets/svg/crosshair-empty.svg" }], maintainer: "MapComplete", startLat: 0, startLon: 0, startZoom: 0, title: "Location input", version: "0" })); }