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 FilteredLayer from "../../Models/FilteredLayer";
 | 
				
			||||||
import {ElementStorage} from "../../Logic/ElementStorage";
 | 
					import {ElementStorage} from "../../Logic/ElementStorage";
 | 
				
			||||||
import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers";
 | 
					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
 | 
					export default class LocationInput
 | 
				
			||||||
    extends BaseUIElement
 | 
					    extends BaseUIElement
 | 
				
			||||||
| 
						 | 
					@ -29,7 +32,7 @@ export default class LocationInput
 | 
				
			||||||
        true
 | 
					        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 _matching_layer: LayerConfig
 | 
				
			||||||
    public readonly leafletMap: UIEventSource<any>
 | 
					    public readonly leafletMap: UIEventSource<any>
 | 
				
			||||||
    public readonly bounds
 | 
					    public readonly bounds
 | 
				
			||||||
| 
						 | 
					@ -40,7 +43,13 @@ export default class LocationInput
 | 
				
			||||||
     * The features to which the input should be snapped
 | 
					     * The features to which the input should be snapped
 | 
				
			||||||
     * @private
 | 
					     * @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 _value: Store<Loc>
 | 
				
			||||||
    private readonly _snappedPoint: Store<any>
 | 
					    private readonly _snappedPoint: Store<any>
 | 
				
			||||||
    private readonly _maxSnapDistance: number
 | 
					    private readonly _maxSnapDistance: number
 | 
				
			||||||
| 
						 | 
					@ -57,10 +66,41 @@ export default class LocationInput
 | 
				
			||||||
        readonly allElements: ElementStorage
 | 
					        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?: {
 | 
					    constructor(options?: {
 | 
				
			||||||
        minZoom?: number
 | 
					        minZoom?: number
 | 
				
			||||||
        mapBackground?: UIEventSource<BaseLayer>
 | 
					        mapBackground?: UIEventSource<BaseLayer>
 | 
				
			||||||
        snapTo?: UIEventSource<{ feature: any }[]>
 | 
					        snapTo?: UIEventSource<{ feature: Feature }[]>
 | 
				
			||||||
        maxSnapDistance?: number
 | 
					        maxSnapDistance?: number
 | 
				
			||||||
        snappedPointTags?: any
 | 
					        snappedPointTags?: any
 | 
				
			||||||
        requiresSnapping?: boolean
 | 
					        requiresSnapping?: boolean
 | 
				
			||||||
| 
						 | 
					@ -75,9 +115,8 @@ export default class LocationInput
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }) {
 | 
					    }) {
 | 
				
			||||||
        super()
 | 
					        super()
 | 
				
			||||||
        this._snapTo = options?.snapTo?.map((features) =>
 | 
					        this._snapToRaw = options?.snapTo?.map(feats => feats.filter(f => f.feature.geometry.type !== "Point"))
 | 
				
			||||||
            features?.filter((feat) => feat.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._maxSnapDistance = options?.maxSnapDistance
 | 
				
			||||||
        this._centerLocation = options?.centerLocation ?? new UIEventSource<Loc>({
 | 
					        this._centerLocation = options?.centerLocation ?? new UIEventSource<Loc>({
 | 
				
			||||||
            lat: 0, lon: 0, zoom: 0
 | 
					            lat: 0, lon: 0, zoom: 0
 | 
				
			||||||
| 
						 | 
					@ -105,36 +144,39 @@ export default class LocationInput
 | 
				
			||||||
                this._matching_layer = LocationInput.matchLayer
 | 
					                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(
 | 
					            this._snappedPoint = this._centerLocation.map(
 | 
				
			||||||
                (loc) => {
 | 
					                (loc) => {
 | 
				
			||||||
                    if (loc === undefined) {
 | 
					                    if (loc === undefined) {
 | 
				
			||||||
                        return undefined
 | 
					                        return undefined
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    // We reproject the location onto every 'snap-to-feature' and select the closest
 | 
					                    // We reproject the location onto every 'snap-to-feature' and select the closest
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    let min = undefined
 | 
					                    let min = undefined
 | 
				
			||||||
                    let matchedWay = undefined
 | 
					                    let matchedWay: Feature<LineString | Polygon> & {properties : {id : WayId}} = undefined
 | 
				
			||||||
                    for (const feature of self._snapTo.data ?? []) {
 | 
					                    for (const feature of self._snapTo.data ?? []) {
 | 
				
			||||||
                        try {
 | 
					                        try {
 | 
				
			||||||
                            const nearestPointOnLine = GeoOperations.nearestPoint(feature.feature, [
 | 
					                            const nearestPointOnLine = GeoOperations.nearestPoint(feature, [
 | 
				
			||||||
                                loc.lon,
 | 
					                                loc.lon,
 | 
				
			||||||
                                loc.lat,
 | 
					                                loc.lat,
 | 
				
			||||||
                            ])
 | 
					                            ])
 | 
				
			||||||
                            if (min === undefined) {
 | 
					                            if (min === undefined) {
 | 
				
			||||||
                                min = nearestPointOnLine
 | 
					                                min = nearestPointOnLine
 | 
				
			||||||
                                matchedWay = feature.feature
 | 
					                                matchedWay = feature
 | 
				
			||||||
                                continue
 | 
					                                continue
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            if (min.properties.dist > nearestPointOnLine.properties.dist) {
 | 
					                            if (min.properties.dist > nearestPointOnLine.properties.dist) {
 | 
				
			||||||
                                min = nearestPointOnLine
 | 
					                                min = nearestPointOnLine
 | 
				
			||||||
                                matchedWay = feature.feature
 | 
					                                matchedWay = feature
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                        } catch (e) {
 | 
					                        } catch (e) {
 | 
				
			||||||
                            console.log(
 | 
					                            console.log(
 | 
				
			||||||
                                "Snapping to a nearest point failed for ",
 | 
					                                "Snapping to a nearest point failed for ",
 | 
				
			||||||
                                feature.feature,
 | 
					                                feature,
 | 
				
			||||||
                                "due to ",
 | 
					                                "due to ",
 | 
				
			||||||
                                e
 | 
					                                e
 | 
				
			||||||
                            )
 | 
					                            )
 | 
				
			||||||
| 
						 | 
					@ -145,6 +187,7 @@ export default class LocationInput
 | 
				
			||||||
                        if (options?.requiresSnapping) {
 | 
					                        if (options?.requiresSnapping) {
 | 
				
			||||||
                            return undefined
 | 
					                            return undefined
 | 
				
			||||||
                        } else {
 | 
					                        } else {
 | 
				
			||||||
 | 
					                            // No match found - the original coordinates are returned as is
 | 
				
			||||||
                            return {
 | 
					                            return {
 | 
				
			||||||
                                type: "Feature",
 | 
					                                type: "Feature",
 | 
				
			||||||
                                properties: options?.snappedPointTags ?? min.properties,
 | 
					                                properties: options?.snappedPointTags ?? min.properties,
 | 
				
			||||||
| 
						 | 
					@ -153,7 +196,13 @@ export default class LocationInput
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    min.properties = options?.snappedPointTags ?? min.properties
 | 
					                    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
 | 
					                    return min
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                [this._snapTo]
 | 
					                [this._snapTo]
 | 
				
			||||||
| 
						 | 
					@ -216,11 +265,11 @@ export default class LocationInput
 | 
				
			||||||
            this.clickLocation.addCallbackAndRunD((location) =>
 | 
					            this.clickLocation.addCallbackAndRunD((location) =>
 | 
				
			||||||
                this._centerLocation.setData(location)
 | 
					                this._centerLocation.setData(location)
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            if (this._snapTo !== undefined) {
 | 
					            if (this._snapToRaw !== undefined) {
 | 
				
			||||||
                // Show the lines to snap to
 | 
					                // 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({
 | 
					                new ShowDataMultiLayer({
 | 
				
			||||||
                    features: StaticFeatureSource.fromDateless(this._snapTo),
 | 
					                    features: StaticFeatureSource.fromDateless(this._snapToRaw),
 | 
				
			||||||
                    zoomToFeatures: false,
 | 
					                    zoomToFeatures: false,
 | 
				
			||||||
                    leafletMap: this.map.leafletMap,
 | 
					                    leafletMap: this.map.leafletMap,
 | 
				
			||||||
                    layers: this._state.filteredLayers,
 | 
					                    layers: this._state.filteredLayers,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue