diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 67ce435b7..83ab997d0 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -19,6 +19,9 @@ import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; import FilteredLayer from "../../Models/FilteredLayer"; import {ElementStorage} from "../../Logic/ElementStorage"; import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import {RelationId, WayId} from "../../Models/OsmFeature"; +import {Feature, LineString, Polygon} from "geojson"; +import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; export default class LocationInput extends BaseUIElement @@ -29,7 +32,7 @@ export default class LocationInput true ) - public readonly snappedOnto: UIEventSource = new UIEventSource(undefined) + public readonly snappedOnto: UIEventSource = new UIEventSource(undefined) public readonly _matching_layer: LayerConfig public readonly leafletMap: UIEventSource public readonly bounds @@ -40,7 +43,13 @@ export default class LocationInput * The features to which the input should be snapped * @private */ - private readonly _snapTo: Store<{ feature: any }[]> + private readonly _snapTo: Store< (Feature & {properties: {id : WayId}})[]> + /** + * The features to which the input should be snapped without cleanup of relations and memberships + * Used for rendering + * @private + */ + private readonly _snapToRaw: Store< {feature: Feature}[]> private readonly _value: Store private readonly _snappedPoint: Store private readonly _maxSnapDistance: number @@ -57,10 +66,41 @@ export default class LocationInput readonly allElements: ElementStorage } + /** + * Given a list of geojson-features, will prepare these features to be snappable: + * - points are removed + * - LineStrings are passed as-is + * - Multipolygons are decomposed into their member ways by downloading them + * + * @private + */ + private static async prepareSnapOnto(features: Feature[]): Promise<(Feature & {properties : {id: WayId}})[]> { + const linesAndPolygon : Feature[] = features.filter(f => f.geometry.type !== "Point") + // Clean the features: multipolygons are split into their it's members + const linestrings : (Feature & {properties: {id: WayId}})[] = [] + for (const feature of linesAndPolygon) { + if(feature.properties.id.startsWith("way")){ + // A normal way - we continue + linestrings.push( feature) + continue + } + + // We have a multipolygon, thus: a relation + // Download the members + const relation = await OsmObject.DownloadObjectAsync( feature.properties.id, 60 * 60) + const members: OsmWay[] = await Promise.all(relation.members + .filter(m => m.type === "way") + .map(m => OsmObject.DownloadObjectAsync( ("way/"+m.ref), 60 * 60))) + linestrings.push(...members.map(m => m.asGeoJson())) + } + return linestrings + + } + constructor(options?: { minZoom?: number mapBackground?: UIEventSource - snapTo?: UIEventSource<{ feature: any }[]> + snapTo?: UIEventSource<{ feature: Feature }[]> maxSnapDistance?: number snappedPointTags?: any requiresSnapping?: boolean @@ -75,9 +115,8 @@ export default class LocationInput } }) { super() - this._snapTo = options?.snapTo?.map((features) => - features?.filter((feat) => feat.feature.geometry.type !== "Point") - ) + this._snapToRaw = options?.snapTo?.map(feats => feats.filter(f => f.feature.geometry.type !== "Point")) + this._snapTo = options?.snapTo?.bind((features) => UIEventSource.FromPromise(LocationInput.prepareSnapOnto(features.map(f => f.feature))))?.map(f => f ?? []) this._maxSnapDistance = options?.maxSnapDistance this._centerLocation = options?.centerLocation ?? new UIEventSource({ lat: 0, lon: 0, zoom: 0 @@ -105,36 +144,39 @@ export default class LocationInput this._matching_layer = LocationInput.matchLayer } + // Calculate the location of the point based by snapping it onto a way + // As a side-effect, the actual snapped-onto way (if any) is saved into 'snappedOnto' this._snappedPoint = this._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 + let matchedWay: Feature & {properties : {id : WayId}} = undefined for (const feature of self._snapTo.data ?? []) { try { - const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [ + const nearestPointOnLine = GeoOperations.nearestPoint(feature, [ loc.lon, loc.lat, ]) if (min === undefined) { min = nearestPointOnLine - matchedWay = feature.feature + matchedWay = feature continue } if (min.properties.dist > nearestPointOnLine.properties.dist) { min = nearestPointOnLine - matchedWay = feature.feature + matchedWay = feature } } catch (e) { console.log( "Snapping to a nearest point failed for ", - feature.feature, + feature, "due to ", e ) @@ -145,6 +187,7 @@ export default class LocationInput if (options?.requiresSnapping) { return undefined } else { + // No match found - the original coordinates are returned as is return { type: "Feature", properties: options?.snappedPointTags ?? min.properties, @@ -153,7 +196,13 @@ export default class LocationInput } } min.properties = options?.snappedPointTags ?? min.properties - self.snappedOnto.setData(matchedWay) + if(matchedWay.properties.id.startsWith("relation/")){ + // We matched a relation instead of a way + console.log("Snapping onto a relation. The relation is", matchedWay) + + + } + self.snappedOnto.setData( matchedWay) return min }, [this._snapTo] @@ -216,11 +265,11 @@ export default class LocationInput this.clickLocation.addCallbackAndRunD((location) => this._centerLocation.setData(location) ) - if (this._snapTo !== undefined) { + if (this._snapToRaw !== undefined) { // Show the lines to snap to - console.log("Constructing the snap-to layer", this._snapTo) + console.log("Constructing the snap-to layer", this._snapToRaw) new ShowDataMultiLayer({ - features: StaticFeatureSource.fromDateless(this._snapTo), + features: StaticFeatureSource.fromDateless(this._snapToRaw), zoomToFeatures: false, leafletMap: this.map.leafletMap, layers: this._state.filteredLayers,