forked from MapComplete/MapComplete
		
	Let locationInput deal with multipolygon correctly
This commit is contained in:
		
							parent
							
								
									28fc81c96f
								
							
						
					
					
						commit
						603fecb107
					
				
					 1 changed files with 64 additions and 15 deletions
				
			
		|  | @ -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<any> = new UIEventSource<any>(undefined) | ||||
|     public readonly snappedOnto: UIEventSource<Feature & { properties : { id : WayId} }> = new UIEventSource(undefined) | ||||
|     public readonly _matching_layer: LayerConfig | ||||
|     public readonly leafletMap: UIEventSource<any> | ||||
|     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<LineString | Polygon> & {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<Loc> | ||||
|     private readonly _snappedPoint: Store<any> | ||||
|     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<LineString | Polygon> & {properties : {id: WayId}})[]> { | ||||
|         const linesAndPolygon : Feature<LineString | Polygon>[] = <any> features.filter(f => f.geometry.type !== "Point") | ||||
|         // Clean the features: multipolygons are split into their it's members
 | ||||
|         const linestrings : (Feature<LineString | Polygon> & {properties: {id: WayId}})[] = [] | ||||
|         for (const feature of linesAndPolygon) { | ||||
|             if(feature.properties.id.startsWith("way")){ | ||||
|                 // A normal way - we continue
 | ||||
|                 linestrings.push(<any> feature) | ||||
|                 continue | ||||
|             } | ||||
| 
 | ||||
|             // We have a multipolygon, thus: a relation
 | ||||
|             // Download the members
 | ||||
|             const relation = await OsmObject.DownloadObjectAsync(<RelationId> feature.properties.id, 60 * 60) | ||||
|             const members: OsmWay[] = await Promise.all(relation.members | ||||
|                 .filter(m => m.type === "way") | ||||
|                 .map(m => OsmObject.DownloadObjectAsync(<WayId> ("way/"+m.ref), 60 * 60))) | ||||
|             linestrings.push(...members.map(m => m.asGeoJson())) | ||||
|         } | ||||
|         return linestrings | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     constructor(options?: { | ||||
|         minZoom?: number | ||||
|         mapBackground?: UIEventSource<BaseLayer> | ||||
|         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<Loc>({ | ||||
|             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<LineString | Polygon> & {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(<any> 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, | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue