import {ReadonlyInputElement} from "./InputElement" import Loc from "../../Models/Loc" import {Store, UIEventSource} from "../../Logic/UIEventSource" import Minimap, {MinimapObj} from "../Base/Minimap" import BaseLayer from "../../Models/BaseLayer" import Combine from "../Base/Combine" import Svg from "../../Svg" import {GeoOperations} from "../../Logic/GeoOperations" import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer" import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource" import LayerConfig from "../../Models/ThemeConfig/LayerConfig" import {BBox} from "../../Logic/BBox" import {FixedUiElement} from "../Base/FixedUiElement" import ShowDataLayer from "../ShowDataLayer/ShowDataLayer" import BaseUIElement from "../BaseUIElement" import Toggle from "./Toggle" import * as matchpoint from "../../assets/layers/matchpoint/matchpoint.json" import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import FilteredLayer from "../../Models/FilteredLayer"; import {ElementStorage} from "../../Logic/ElementStorage"; export default class LocationInput extends BaseUIElement implements ReadonlyInputElement, MinimapObj { private static readonly matchLayer = new LayerConfig( matchpoint, "LocationInput.matchpoint", true ) public readonly snappedOnto: UIEventSource = new UIEventSource(undefined) public readonly _matching_layer: LayerConfig public readonly leafletMap: UIEventSource public readonly bounds public readonly location private readonly _centerLocation: UIEventSource private readonly mapBackground: UIEventSource /** * The features to which the input should be snapped * @private */ private readonly _snapTo: Store<{ feature: any }[]> private readonly _value: Store private readonly _snappedPoint: Store private readonly _maxSnapDistance: number private readonly _snappedPointTags: any private readonly _bounds: UIEventSource private readonly map: BaseUIElement & MinimapObj private readonly clickLocation: UIEventSource private readonly _minZoom: number private readonly _state: { readonly filteredLayers: Store; readonly backgroundLayer: UIEventSource; readonly layoutToUse: LayoutConfig; readonly selectedElement: UIEventSource; readonly allElements: ElementStorage } constructor(options: { minZoom?: number mapBackground?: UIEventSource snapTo?: UIEventSource<{ feature: any }[]> maxSnapDistance?: number snappedPointTags?: any requiresSnapping?: boolean centerLocation: UIEventSource bounds?: UIEventSource state: { readonly filteredLayers: Store; readonly backgroundLayer: UIEventSource; readonly layoutToUse: LayoutConfig; readonly selectedElement: UIEventSource; readonly allElements: ElementStorage } }) { 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 this._bounds = options.bounds this._minZoom = options.minZoom this._state = options.state if (this._snapTo === undefined) { this._value = this._centerLocation } else { const self = this if (self._snappedPointTags !== undefined) { const layout = this._state.layoutToUse let matchingLayer = LocationInput.matchLayer for (const layer of layout.layers) { if (layer.source.osmTags.matchesProperties(self._snappedPointTags)) { matchingLayer = layer } } this._matching_layer = matchingLayer } else { this._matching_layer = LocationInput.matchLayer } 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 ?? []) { try { 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 } } catch (e) { console.log( "Snapping to a nearest point failed for ", feature.feature, "due to ", e ) } } if (min === undefined || min.properties.dist * 1000 > self._maxSnapDistance) { if (options.requiresSnapping) { return undefined } else { return { type: "Feature", properties: options.snappedPointTags ?? min.properties, geometry: {type: "Point", coordinates: [loc.lon, loc.lat]}, } } } 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 ?? this._state?.backgroundLayer this.SetClass("block h-full") this.clickLocation = new UIEventSource(undefined) this.map = Minimap.createMiniMap({ location: this._centerLocation, background: this.mapBackground, attribution: this.mapBackground !== this._state?.backgroundLayer, lastClickLocation: this.clickLocation, bounds: this._bounds, addLayerControl: true, }) this.leafletMap = this.map.leafletMap this.location = this.map.location } GetValue(): Store { return this._value } IsValid(t: Loc): boolean { return t !== undefined } installBounds(factor: number | BBox, showRange?: boolean): void { this.map.installBounds(factor, showRange) } protected InnerConstructElement(): HTMLElement { try { const self = this const hasMoved = new UIEventSource(false) const startLocation = {...this._centerLocation.data} this._centerLocation.addCallbackD((newLocation) => { const f = 100000 console.log(newLocation.lon, startLocation.lon) const diff = Math.abs(newLocation.lon * f - startLocation.lon * f) + Math.abs(newLocation.lat * f - startLocation.lat * f) if (diff < 1) { return } hasMoved.setData(true) return true }) this.clickLocation.addCallbackAndRunD((location) => this._centerLocation.setData(location) ) if (this._snapTo !== undefined) { // Show the lines to snap to console.log("Constructing the snap-to layer", this._snapTo) new ShowDataMultiLayer({ features: StaticFeatureSource.fromDateless(this._snapTo), zoomToFeatures: false, leafletMap: this.map.leafletMap, layers: this._state.filteredLayers, }) // Show the central point const matchPoint = this._snappedPoint.map((loc) => { if (loc === undefined) { return [] } return [{feature: loc}] }) console.log("Constructing the match layer", matchPoint) new ShowDataLayer({ features: StaticFeatureSource.fromDateless(matchPoint), zoomToFeatures: false, leafletMap: this.map.leafletMap, layerToShow: this._matching_layer, state: this._state, selectedElement: this._state.selectedElement, }) } this.mapBackground.map( (layer) => { const leaflet = this.map.leafletMap.data if (leaflet === undefined || layer === undefined) { return } leaflet.setMaxZoom(layer.max_zoom) leaflet.setMinZoom(self._minZoom ?? layer.max_zoom - 2) leaflet.setZoom(layer.max_zoom - 1) }, [this.map.leafletMap] ) const animatedHand = Svg.hand_ui() .SetStyle("width: 2rem; height: unset;") .SetClass("hand-drag-animation block pointer-events-none") 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%; opacity: 0.5" ), new Toggle(undefined, animatedHand, hasMoved) .SetClass("block w-0 h-0 z-10 relative") .SetStyle("left: calc(50% + 3rem); top: calc(50% + 2rem); opacity: 0.7"), this.map.SetClass("z-0 relative block w-full h-full bg-gray-100"), ]).ConstructElement() } catch (e) { console.error("Could not generate LocationInputElement:", e) return new FixedUiElement("Constructing a locationInput failed due to" + e) .SetClass("alert") .ConstructElement() } } TakeScreenshot(format: "image"): Promise; TakeScreenshot(format: "blob"): Promise; TakeScreenshot(format: "image" | "blob"): Promise; TakeScreenshot(format: "image" | "blob"): Promise { return this.map.TakeScreenshot(format) } }