forked from MapComplete/MapComplete
		
	Refactoring of GPS-location (uses featureSource too now), factoring out state, add ReplaceGeometryAction and conflation example
This commit is contained in:
		
							parent
							
								
									1db54f3c8e
								
							
						
					
					
						commit
						2484848cd6
					
				
					 37 changed files with 1035 additions and 467 deletions
				
			
		| 
						 | 
				
			
			@ -1,13 +1,16 @@
 | 
			
		|||
import * as L from "leaflet";
 | 
			
		||||
import {UIEventSource} from "../UIEventSource";
 | 
			
		||||
import Svg from "../../Svg";
 | 
			
		||||
import Img from "../../UI/Base/Img";
 | 
			
		||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
 | 
			
		||||
import {VariableUiElement} from "../../UI/Base/VariableUIElement";
 | 
			
		||||
import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig";
 | 
			
		||||
import {QueryParameters} from "../Web/QueryParameters";
 | 
			
		||||
import FeatureSource from "../FeatureSource/FeatureSource";
 | 
			
		||||
import StaticFeatureSource from "../FeatureSource/Sources/StaticFeatureSource";
 | 
			
		||||
 | 
			
		||||
export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		||||
    
 | 
			
		||||
    public readonly currentLocation : FeatureSource
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Wether or not the geolocation is active, aka the user requested the current location
 | 
			
		||||
     * @private
 | 
			
		||||
| 
						 | 
				
			
			@ -25,20 +28,12 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    private readonly _permission: UIEventSource<string>;
 | 
			
		||||
    /***
 | 
			
		||||
     * The marker on the map, in order to update it
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    private _marker: L.Marker;
 | 
			
		||||
    /**
 | 
			
		||||
     * Literally: _currentGPSLocation.data != undefined
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    private readonly _hasLocation: UIEventSource<boolean>;
 | 
			
		||||
    private readonly _currentGPSLocation: UIEventSource<{
 | 
			
		||||
        latlng: any;
 | 
			
		||||
        accuracy: number;
 | 
			
		||||
    }>;
 | 
			
		||||
    private readonly _currentGPSLocation: UIEventSource<Coordinates>;
 | 
			
		||||
    /**
 | 
			
		||||
     * Kept in order to update the marker
 | 
			
		||||
     * @private
 | 
			
		||||
| 
						 | 
				
			
			@ -63,8 +58,8 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
    private readonly _layoutToUse: LayoutConfig;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
 | 
			
		||||
        leafletMap: UIEventSource<L.Map>,
 | 
			
		||||
        currentGPSLocation: UIEventSource<Coordinates>,
 | 
			
		||||
        leafletMap: UIEventSource<any>,
 | 
			
		||||
        layoutToUse: LayoutConfig
 | 
			
		||||
    ) {
 | 
			
		||||
        const hasLocation = currentGPSLocation.map(
 | 
			
		||||
| 
						 | 
				
			
			@ -182,10 +177,25 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        this.currentLocation  = new StaticFeatureSource([], false)
 | 
			
		||||
        this._currentGPSLocation.addCallback((location) => {
 | 
			
		||||
            self._previousLocationGrant.setData("granted");
 | 
			
		||||
 | 
			
		||||
            const feature = {
 | 
			
		||||
                "type": "Feature",
 | 
			
		||||
                properties: {
 | 
			
		||||
                    "user:location":"yes",
 | 
			
		||||
                    "accuracy":location.accuracy,
 | 
			
		||||
                    "speed":location.speed,
 | 
			
		||||
                },
 | 
			
		||||
                geometry:{
 | 
			
		||||
                    type:"Point",
 | 
			
		||||
                    coordinates: [location.longitude, location.latitude],
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            self.currentLocation.features.setData([{feature, freshness: new Date()}])
 | 
			
		||||
            
 | 
			
		||||
            const timeSinceRequest =
 | 
			
		||||
                (new Date().getTime() - (self._lastUserRequest?.getTime() ?? 0)) / 1000;
 | 
			
		||||
            if (timeSinceRequest < 30) {
 | 
			
		||||
| 
						 | 
				
			
			@ -194,33 +204,8 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
                self.MoveToCurrentLoction();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            let color = "#1111cc";
 | 
			
		||||
            try {
 | 
			
		||||
                color = getComputedStyle(document.body).getPropertyValue(
 | 
			
		||||
                    "--catch-detail-color"
 | 
			
		||||
                );
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
                console.error(e);
 | 
			
		||||
            }
 | 
			
		||||
            const icon = L.icon({
 | 
			
		||||
                iconUrl: Img.AsData(Svg.location.replace(/#000000/g, color).replace(/#000/g, color)),
 | 
			
		||||
                iconSize: [40, 40], // size of the icon
 | 
			
		||||
                iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const map = self._leafletMap.data;
 | 
			
		||||
            if(map === undefined){
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            const newMarker = L.marker(location.latlng, {icon: icon});
 | 
			
		||||
            newMarker.addTo(map);
 | 
			
		||||
 | 
			
		||||
            if (self._marker !== undefined) {
 | 
			
		||||
                map.removeLayer(self._marker);
 | 
			
		||||
            }
 | 
			
		||||
            self._marker = newMarker;
 | 
			
		||||
        });
 | 
			
		||||
  
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private init(askPermission: boolean, forceZoom: boolean) {
 | 
			
		||||
| 
						 | 
				
			
			@ -261,8 +246,8 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
        this._lastUserRequest = undefined;
 | 
			
		||||
 | 
			
		||||
        if (
 | 
			
		||||
            this._currentGPSLocation.data.latlng[0] === 0 &&
 | 
			
		||||
            this._currentGPSLocation.data.latlng[1] === 0
 | 
			
		||||
            this._currentGPSLocation.data.latitude === 0 &&
 | 
			
		||||
            this._currentGPSLocation.data.longitude === 0
 | 
			
		||||
        ) {
 | 
			
		||||
            console.debug("Not moving to GPS-location: it is null island");
 | 
			
		||||
            return;
 | 
			
		||||
| 
						 | 
				
			
			@ -275,20 +260,20 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
            if (b !== true) {
 | 
			
		||||
                // B is an array with our locklocation
 | 
			
		||||
                inRange =
 | 
			
		||||
                    b[0][0] <= location.latlng[0] &&
 | 
			
		||||
                    location.latlng[0] <= b[1][0] &&
 | 
			
		||||
                    b[0][1] <= location.latlng[1] &&
 | 
			
		||||
                    location.latlng[1] <= b[1][1];
 | 
			
		||||
                    b[0][0] <= location.latitude &&
 | 
			
		||||
                    location.latitude <= b[1][0] &&
 | 
			
		||||
                    b[0][1] <= location.longitude &&
 | 
			
		||||
                    location.longitude <= b[1][1];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if (!inRange) {
 | 
			
		||||
            console.log(
 | 
			
		||||
                "Not zooming to GPS location: out of bounds",
 | 
			
		||||
                b,
 | 
			
		||||
                location.latlng
 | 
			
		||||
                location
 | 
			
		||||
            );
 | 
			
		||||
        } else {
 | 
			
		||||
            this._leafletMap.data.setView(location.latlng, targetZoom);
 | 
			
		||||
            this._leafletMap.data.setView([location.latitude, location.longitude], targetZoom);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -312,10 +297,7 @@ export default class GeoLocationHandler extends VariableUiElement {
 | 
			
		|||
 | 
			
		||||
        navigator.geolocation.watchPosition(
 | 
			
		||||
            function (position) {
 | 
			
		||||
                self._currentGPSLocation.setData({
 | 
			
		||||
                    latlng: [position.coords.latitude, position.coords.longitude],
 | 
			
		||||
                    accuracy: position.coords.accuracy,
 | 
			
		||||
                });
 | 
			
		||||
                self._currentGPSLocation.setData(position.coords);
 | 
			
		||||
            },
 | 
			
		||||
            function () {
 | 
			
		||||
                console.warn("Could not get location with navigator.geolocation");
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -116,6 +116,11 @@ export class BBox {
 | 
			
		|||
    getSouth() {
 | 
			
		||||
        return this.minLat
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    contains(lonLat: [number, number]){
 | 
			
		||||
        return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat
 | 
			
		||||
        && this.minLon<= lonLat[0] && lonLat[0] <= this.maxLon
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pad(factor: number, maxIncrease = 2): BBox {
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -228,11 +228,15 @@ export default class FeaturePipeline {
 | 
			
		|||
        })
 | 
			
		||||
        
 | 
			
		||||
        if(state.layoutToUse.trackAllNodes){
 | 
			
		||||
             new FullNodeDatabaseSource(state, osmFeatureSource, tile => {
 | 
			
		||||
             const fullNodeDb = new FullNodeDatabaseSource(
 | 
			
		||||
                 state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0],
 | 
			
		||||
                 tile => {
 | 
			
		||||
                 new RegisteringAllFromFeatureSourceActor(tile)
 | 
			
		||||
                 perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
 | 
			
		||||
                 tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
 | 
			
		||||
             })
 | 
			
		||||
 | 
			
		||||
            osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => fullNodeDb.handleOsmJson(osmJson, tileId))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -70,7 +70,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource {
 | 
			
		|||
                                const w = new OsmWay(change.id)
 | 
			
		||||
                                w.tags = tags
 | 
			
		||||
                                w.nodes = change.changes["nodes"]
 | 
			
		||||
                                w.coordinates = change.changes["coordinates"].map(coor => coor.reverse())
 | 
			
		||||
                                w.coordinates = change.changes["coordinates"].map(coor => [coor[1], coor[0]])
 | 
			
		||||
                                add(w.asGeoJson())
 | 
			
		||||
                                break;
 | 
			
		||||
                            case "relation":
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,7 +32,7 @@ export default class RenderingMultiPlexerFeatureSource {
 | 
			
		|||
                const withIndex: (any & { pointRenderingIndex: number | undefined, lineRenderingIndex: number | undefined })[] = [];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                function addAsPoint(feat, rendering, coordinate) {
 | 
			
		||||
                function addAsPoint(feat, rendering, coordinate)     {
 | 
			
		||||
                    const patched = {
 | 
			
		||||
                        ...feat,
 | 
			
		||||
                        pointRenderingIndex: rendering.index
 | 
			
		||||
| 
						 | 
				
			
			@ -46,8 +46,6 @@ export default class RenderingMultiPlexerFeatureSource {
 | 
			
		|||
 | 
			
		||||
                for (const f of features) {
 | 
			
		||||
                    const feat = f.feature;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
                    if (feat.geometry.type === "Point") {
 | 
			
		||||
 | 
			
		||||
                        for (const rendering of pointRenderings) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,30 +2,103 @@ import TileHierarchy from "./TileHierarchy";
 | 
			
		|||
import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource";
 | 
			
		||||
import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject";
 | 
			
		||||
import SimpleFeatureSource from "../Sources/SimpleFeatureSource";
 | 
			
		||||
import {UIEventSource} from "../../UIEventSource";
 | 
			
		||||
import FilteredLayer from "../../../Models/FilteredLayer";
 | 
			
		||||
import {TagsFilter} from "../../Tags/TagsFilter";
 | 
			
		||||
import OsmChangeAction from "../../Osm/Actions/OsmChangeAction";
 | 
			
		||||
import StaticFeatureSource from "../Sources/StaticFeatureSource";
 | 
			
		||||
import {OsmConnection} from "../../Osm/OsmConnection";
 | 
			
		||||
import {GeoOperations} from "../../GeoOperations";
 | 
			
		||||
import {Utils} from "../../../Utils";
 | 
			
		||||
import {UIEventSource} from "../../UIEventSource";
 | 
			
		||||
import {BBox} from "../../BBox";
 | 
			
		||||
import FeaturePipeline from "../FeaturePipeline";
 | 
			
		||||
import {Tag} from "../../Tags/Tag";
 | 
			
		||||
import LayoutConfig from "../../../Models/ThemeConfig/LayoutConfig";
 | 
			
		||||
import {ChangeDescription} from "../../Osm/Actions/ChangeDescription";
 | 
			
		||||
import CreateNewNodeAction from "../../Osm/Actions/CreateNewNodeAction";
 | 
			
		||||
import ChangeTagAction from "../../Osm/Actions/ChangeTagAction";
 | 
			
		||||
import {And} from "../../Tags/And";
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSource & Tiled> {
 | 
			
		||||
    public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
 | 
			
		||||
    private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
 | 
			
		||||
    private readonly layer : FilteredLayer
 | 
			
		||||
    
 | 
			
		||||
    private readonly layer: FilteredLayer
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        state: {
 | 
			
		||||
            readonly filteredLayers: UIEventSource<FilteredLayer[]>},
 | 
			
		||||
            osmFeatureSource: { rawDataHandlers: ((data: any, tileId: number) => void)[] }, 
 | 
			
		||||
        layer: FilteredLayer,
 | 
			
		||||
        onTileLoaded: ((tile: Tiled & FeatureSourceForLayer) => void)) {
 | 
			
		||||
        this.onTileLoaded = onTileLoaded
 | 
			
		||||
        this.layer = state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0]
 | 
			
		||||
        if(this.layer === undefined){
 | 
			
		||||
            throw "Weird: tracking all nodes, but layer 'type_node' is not defined"
 | 
			
		||||
        this.layer = layer;
 | 
			
		||||
        if (this.layer === undefined) {
 | 
			
		||||
            throw "Layer is undefined"
 | 
			
		||||
        }
 | 
			
		||||
        const self = this
 | 
			
		||||
        osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => self.handleOsmXml(osmJson, tileId))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private handleOsmXml(osmJson: any, tileId: number) {
 | 
			
		||||
    /**
 | 
			
		||||
     * Given a list of coordinates, will search already existing OSM-points to snap onto.
 | 
			
		||||
     * Either the geometry will be moved OR the existing point will be moved, depending on configuration and tags.
 | 
			
		||||
     * This requires the 'type_node'-layer to be activated
 | 
			
		||||
     */
 | 
			
		||||
    public static MergePoints(
 | 
			
		||||
        state: {
 | 
			
		||||
            filteredLayers: UIEventSource<FilteredLayer[]>,
 | 
			
		||||
            featurePipeline: FeaturePipeline,
 | 
			
		||||
            layoutToUse: LayoutConfig
 | 
			
		||||
        },
 | 
			
		||||
        newGeometryLngLats: [number, number][],
 | 
			
		||||
        configs: ConflationConfig[],
 | 
			
		||||
    ) {
 | 
			
		||||
        const typeNode = state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0]
 | 
			
		||||
        if (typeNode === undefined) {
 | 
			
		||||
            throw "Type Node layer is not defined. Add 'type_node' as layer to your layerconfig to use this feature"
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const bbox = new BBox(newGeometryLngLats)
 | 
			
		||||
        const bbox_padded = bbox.pad(1.2)
 | 
			
		||||
        const allNodes: any[] = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox).map(tile => tile.filter(
 | 
			
		||||
            feature => bbox_padded.contains(GeoOperations.centerpointCoordinates(feature))
 | 
			
		||||
        )))
 | 
			
		||||
        // The strategy: for every point of the new geometry, we search a point that is closeby and matches
 | 
			
		||||
        // If multiple options match, we choose the most optimal (aka closest)
 | 
			
		||||
 | 
			
		||||
        const maxDistance = Math.max(...configs.map(c => c.withinRangeOfM))
 | 
			
		||||
        for (const coordinate of newGeometryLngLats) {
 | 
			
		||||
 | 
			
		||||
            let closestNode = undefined;
 | 
			
		||||
            let closestNodeDistance = undefined
 | 
			
		||||
            for (const node of allNodes) {
 | 
			
		||||
                const d = GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(node), coordinate)
 | 
			
		||||
                if (d > maxDistance) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                let matchesSomeConfig = false
 | 
			
		||||
                for (const config of configs) {
 | 
			
		||||
                    if (d > config.withinRangeOfM) {
 | 
			
		||||
                        continue
 | 
			
		||||
                    }
 | 
			
		||||
                    if (!config.ifMatches.matchesProperties(node.properties)) {
 | 
			
		||||
                        continue
 | 
			
		||||
                    }
 | 
			
		||||
                    matchesSomeConfig = true;
 | 
			
		||||
                }
 | 
			
		||||
                if (!matchesSomeConfig) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                if (closestNode === undefined || closestNodeDistance > d) {
 | 
			
		||||
                    closestNode = node;
 | 
			
		||||
                    closestNodeDistance = d;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
   
 | 
			
		||||
    public handleOsmJson(osmJson: any, tileId: number) {
 | 
			
		||||
 | 
			
		||||
        const allObjects = OsmObject.ParseObjects(osmJson.elements)
 | 
			
		||||
        const nodesById = new Map<number, OsmNode>()
 | 
			
		||||
| 
						 | 
				
			
			@ -57,7 +130,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
 | 
			
		|||
        })
 | 
			
		||||
        const now = new Date()
 | 
			
		||||
        const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
 | 
			
		||||
            feature: osmNode.asGeoJson(),freshness: now
 | 
			
		||||
            feature: osmNode.asGeoJson(), freshness: now
 | 
			
		||||
        }))
 | 
			
		||||
 | 
			
		||||
        const featureSource = new SimpleFeatureSource(this.layer, tileId)
 | 
			
		||||
| 
						 | 
				
			
			@ -66,5 +139,12 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
 | 
			
		|||
        this.onTileLoaded(featureSource)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ConflationConfig {
 | 
			
		||||
    withinRangeOfM: number,
 | 
			
		||||
    ifMatches: TagsFilter,
 | 
			
		||||
    mode: "reuse_osm_point" | "move_osm_point"
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -68,7 +68,7 @@ export default class OsmFeatureSource {
 | 
			
		|||
                    console.log("Tile download", Tiles.tile_from_index(neededTile).join("/"), "started")
 | 
			
		||||
                    self.downloadedTiles.add(neededTile)
 | 
			
		||||
                    self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => {
 | 
			
		||||
                        console.log("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded")
 | 
			
		||||
                        console.debug("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded")
 | 
			
		||||
                    })
 | 
			
		||||
                }
 | 
			
		||||
            } catch (e) {
 | 
			
		||||
| 
						 | 
				
			
			@ -98,7 +98,7 @@ export default class OsmFeatureSource {
 | 
			
		|||
            console.log("Attempting to get tile", z, x, y, "from the osm api")
 | 
			
		||||
            const osmJson = await Utils.downloadJson(url)
 | 
			
		||||
            try {
 | 
			
		||||
                console.log("Got tile", z, x, y, "from the osm api")
 | 
			
		||||
                console.debug("Got tile", z, x, y, "from the osm api")
 | 
			
		||||
                this.rawDataHandlers.forEach(handler => handler(osmJson, Tiles.tile_index(z, x, y)))
 | 
			
		||||
                const geojson = OsmToGeoJson.default(osmJson,
 | 
			
		||||
                    // @ts-ignore
 | 
			
		||||
| 
						 | 
				
			
			@ -110,10 +110,8 @@ export default class OsmFeatureSource {
 | 
			
		|||
                // We only keep what is needed
 | 
			
		||||
 | 
			
		||||
                geojson.features = geojson.features.filter(feature => this.allowedTags.matchesProperties(feature.properties))
 | 
			
		||||
 | 
			
		||||
                geojson.features.forEach(f => f.properties["_backend"] = this._backend)
 | 
			
		||||
                
 | 
			
		||||
                console.log("Tile geojson:", z, x, y, "is", geojson)
 | 
			
		||||
                const index = Tiles.tile_index(z, x, y);
 | 
			
		||||
                new PerLayerFeatureSourceSplitter(this.filteredLayers,
 | 
			
		||||
                    this.handleTile,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,7 @@ export default class ChangeTagAction extends OsmChangeAction {
 | 
			
		|||
 | 
			
		||||
    constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any, meta: {
 | 
			
		||||
        theme: string,
 | 
			
		||||
        changeType: "answer" | "soft-delete" | "add-image"
 | 
			
		||||
        changeType: "answer" | "soft-delete" | "add-image" | string
 | 
			
		||||
    }) {
 | 
			
		||||
        super();
 | 
			
		||||
        this._elementId = elementId;
 | 
			
		||||
| 
						 | 
				
			
			@ -27,11 +27,16 @@ export default class ChangeTagAction extends OsmChangeAction {
 | 
			
		|||
        const key = kv.k;
 | 
			
		||||
        const value = kv.v;
 | 
			
		||||
        if (key === undefined || key === null) {
 | 
			
		||||
            console.log("Invalid key");
 | 
			
		||||
            console.error("Invalid key:", key);
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
        if (value === undefined || value === null) {
 | 
			
		||||
            console.log("Invalid value for ", key);
 | 
			
		||||
            console.error("Invalid value for ", key,":", value);
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if(typeof value !== "string"){
 | 
			
		||||
            console.error("Invalid value for ", key, "as it is not a string:", value)
 | 
			
		||||
            return undefined;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,39 +4,25 @@ import {Changes} from "../Changes";
 | 
			
		|||
import {Tag} from "../../Tags/Tag";
 | 
			
		||||
import CreateNewNodeAction from "./CreateNewNodeAction";
 | 
			
		||||
import {And} from "../../Tags/And";
 | 
			
		||||
import {TagsFilter} from "../../Tags/TagsFilter";
 | 
			
		||||
 | 
			
		||||
export default class CreateNewWayAction extends OsmChangeAction {
 | 
			
		||||
    public newElementId: string = undefined
 | 
			
		||||
    private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
 | 
			
		||||
    private readonly tags: Tag[];
 | 
			
		||||
    private readonly _options: {
 | 
			
		||||
        theme: string, existingPointHandling?: {
 | 
			
		||||
            withinRangeOfM: number,
 | 
			
		||||
            ifMatches?: TagsFilter,
 | 
			
		||||
            mode: "reuse_osm_point" | "move_osm_point"
 | 
			
		||||
        } []
 | 
			
		||||
        theme: string
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /***
 | 
			
		||||
     * Creates a new way to upload to OSM
 | 
			
		||||
     * @param tags: the tags to apply to the wya
 | 
			
		||||
     * @param tags: the tags to apply to the way
 | 
			
		||||
     * @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used
 | 
			
		||||
     * @param options
 | 
			
		||||
     */
 | 
			
		||||
    constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[],
 | 
			
		||||
                options: {
 | 
			
		||||
                    theme: string,
 | 
			
		||||
                    /**
 | 
			
		||||
                     * IF specified, an existing OSM-point within this range and satisfying the condition 'ifMatches' will be used instead of a new coordinate.
 | 
			
		||||
                     * If multiple points are possible, only the closest point is considered
 | 
			
		||||
                     */
 | 
			
		||||
                    existingPointHandling?: {
 | 
			
		||||
                        withinRangeOfM: number,
 | 
			
		||||
                        ifMatches?: TagsFilter,
 | 
			
		||||
                        mode: "reuse_osm_point" | "move_osm_point"
 | 
			
		||||
                    } []
 | 
			
		||||
                    theme: string
 | 
			
		||||
                }) {
 | 
			
		||||
        super()
 | 
			
		||||
        this.coordinates = coordinates;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										232
									
								
								Logic/Osm/Actions/ReplaceGeometryAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										232
									
								
								Logic/Osm/Actions/ReplaceGeometryAction.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,232 @@
 | 
			
		|||
import OsmChangeAction from "./OsmChangeAction";
 | 
			
		||||
import {Changes} from "../Changes";
 | 
			
		||||
import {ChangeDescription} from "./ChangeDescription";
 | 
			
		||||
import {Tag} from "../../Tags/Tag";
 | 
			
		||||
import FeatureSource from "../../FeatureSource/FeatureSource";
 | 
			
		||||
import {OsmNode, OsmObject, OsmWay} from "../OsmObject";
 | 
			
		||||
import {GeoOperations} from "../../GeoOperations";
 | 
			
		||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
 | 
			
		||||
import CreateNewNodeAction from "./CreateNewNodeAction";
 | 
			
		||||
import ChangeTagAction from "./ChangeTagAction";
 | 
			
		||||
import {And} from "../../Tags/And";
 | 
			
		||||
import {Utils} from "../../../Utils";
 | 
			
		||||
import {OsmConnection} from "../OsmConnection";
 | 
			
		||||
 | 
			
		||||
export default class ReplaceGeometryAction extends OsmChangeAction {
 | 
			
		||||
    private readonly feature: any;
 | 
			
		||||
    private readonly state: {
 | 
			
		||||
        osmConnection: OsmConnection
 | 
			
		||||
    };
 | 
			
		||||
    private readonly wayToReplaceId: string;
 | 
			
		||||
    private readonly theme: string;
 | 
			
		||||
    private readonly targetCoordinates: [number, number][];
 | 
			
		||||
    private readonly newTags: Tag[] | undefined;
 | 
			
		||||
 | 
			
		||||
    constructor(
 | 
			
		||||
        state: {
 | 
			
		||||
            osmConnection: OsmConnection
 | 
			
		||||
        },
 | 
			
		||||
        feature: any,
 | 
			
		||||
        wayToReplaceId: string,
 | 
			
		||||
        options: {
 | 
			
		||||
            theme: string,
 | 
			
		||||
            newTags?: Tag[]
 | 
			
		||||
        }
 | 
			
		||||
    ) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.state = state;
 | 
			
		||||
        this.feature = feature;
 | 
			
		||||
        this.wayToReplaceId = wayToReplaceId;
 | 
			
		||||
        this.theme = options.theme;
 | 
			
		||||
 | 
			
		||||
        const geom = this.feature.geometry
 | 
			
		||||
        let coordinates: [number, number][]
 | 
			
		||||
        if (geom.type === "LineString") {
 | 
			
		||||
            coordinates = geom.coordinates
 | 
			
		||||
        } else if (geom.type === "Polygon") {
 | 
			
		||||
            coordinates = geom.coordinates[0]
 | 
			
		||||
        }
 | 
			
		||||
        this.targetCoordinates = coordinates
 | 
			
		||||
        this.newTags = options.newTags
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async GetPreview(): Promise<FeatureSource> {
 | 
			
		||||
        const {closestIds, allNodesById} = await this.GetClosestIds();
 | 
			
		||||
        const preview = closestIds.map((newId, i) => {
 | 
			
		||||
            if (newId === undefined) {
 | 
			
		||||
                return {
 | 
			
		||||
                    type: "Feature",
 | 
			
		||||
                    properties: {
 | 
			
		||||
                        "newpoint": "yes",
 | 
			
		||||
                        "id": "replace-geometry-move-" + i
 | 
			
		||||
                    },
 | 
			
		||||
                    geometry: {
 | 
			
		||||
                        type: "Point",
 | 
			
		||||
                        coordinates: this.targetCoordinates[i]
 | 
			
		||||
                    }
 | 
			
		||||
                };
 | 
			
		||||
            }
 | 
			
		||||
            const origPoint = allNodesById.get(newId).centerpoint()
 | 
			
		||||
            return {
 | 
			
		||||
                type: "Feature",
 | 
			
		||||
                properties: {
 | 
			
		||||
                    "move": "yes",
 | 
			
		||||
                    "osm-id": newId,
 | 
			
		||||
                    "id": "replace-geometry-move-" + i
 | 
			
		||||
                },
 | 
			
		||||
                geometry: {
 | 
			
		||||
                    type: "LineString",
 | 
			
		||||
                    coordinates: [[origPoint[1], origPoint[0]], this.targetCoordinates[i]]
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        })
 | 
			
		||||
        return new StaticFeatureSource(preview, false)
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
 | 
			
		||||
 | 
			
		||||
        const allChanges: ChangeDescription[] = []
 | 
			
		||||
        const actualIdsToUse: number[] = []
 | 
			
		||||
 | 
			
		||||
        const {closestIds, osmWay} = await this.GetClosestIds()
 | 
			
		||||
 | 
			
		||||
        for (let i = 0; i < closestIds.length; i++) {
 | 
			
		||||
            const closestId = closestIds[i];
 | 
			
		||||
            const [lon, lat] = this.targetCoordinates[i]
 | 
			
		||||
            if (closestId === undefined) {
 | 
			
		||||
 | 
			
		||||
                const newNodeAction = new CreateNewNodeAction(
 | 
			
		||||
                    [],
 | 
			
		||||
                    lat, lon,
 | 
			
		||||
                    {
 | 
			
		||||
                        allowReuseOfPreviouslyCreatedPoints: true,
 | 
			
		||||
                        theme: this.theme, changeType: null
 | 
			
		||||
                    })
 | 
			
		||||
                const changeDescr = await newNodeAction.CreateChangeDescriptions(changes)
 | 
			
		||||
                allChanges.push(...changeDescr)
 | 
			
		||||
                actualIdsToUse.push(newNodeAction.newElementIdNumber)
 | 
			
		||||
 | 
			
		||||
            } else {
 | 
			
		||||
                const change = <ChangeDescription>{
 | 
			
		||||
                    id: closestId,
 | 
			
		||||
                    type: "node",
 | 
			
		||||
                    meta: {
 | 
			
		||||
                        theme: this.theme,
 | 
			
		||||
                        changeType: "move"
 | 
			
		||||
                    },
 | 
			
		||||
                    changes: {lon, lat}
 | 
			
		||||
                }
 | 
			
		||||
                actualIdsToUse.push(closestId)
 | 
			
		||||
                allChanges.push(change)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        if (this.newTags !== undefined && this.newTags.length > 0) {
 | 
			
		||||
            const addExtraTags = new ChangeTagAction(
 | 
			
		||||
                this.wayToReplaceId,
 | 
			
		||||
                new And(this.newTags),
 | 
			
		||||
                osmWay.tags, {
 | 
			
		||||
                    theme: this.theme,
 | 
			
		||||
                    changeType: "conflation"
 | 
			
		||||
                }
 | 
			
		||||
            )
 | 
			
		||||
            allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // AT the very last: actually change the nodes of the way!
 | 
			
		||||
        allChanges.push({
 | 
			
		||||
            type: "way",
 | 
			
		||||
            id: osmWay.id,
 | 
			
		||||
            changes: {
 | 
			
		||||
                nodes: actualIdsToUse,
 | 
			
		||||
                coordinates: this.targetCoordinates
 | 
			
		||||
            },
 | 
			
		||||
            meta: {
 | 
			
		||||
                theme: this.theme,
 | 
			
		||||
                changeType: "conflation"
 | 
			
		||||
            }
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        return allChanges
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * For 'this.feature`, gets a corresponding closest node that alreay exsists
 | 
			
		||||
     * @constructor
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> {
 | 
			
		||||
        // TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them)
 | 
			
		||||
        // TODO FIXME: if a new point has to be created, snap to already existing ways
 | 
			
		||||
        // TODO FIXME: reuse points if they are the same in the target coordinates
 | 
			
		||||
        const splitted = this.wayToReplaceId.split("/");
 | 
			
		||||
        const type = splitted[0];
 | 
			
		||||
        const idN = Number(splitted[1]);
 | 
			
		||||
        if (idN < 0 || type !== "way") {
 | 
			
		||||
            throw "Invalid ID to conflate: " + this.wayToReplaceId
 | 
			
		||||
        }
 | 
			
		||||
        const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
 | 
			
		||||
        const rawData = await Utils.downloadJsonCached(url, 1000)
 | 
			
		||||
        const parsed = OsmObject.ParseObjects(rawData.elements);
 | 
			
		||||
        const allNodesById = new Map<number, OsmNode>()
 | 
			
		||||
        const allNodes = parsed.filter(o => o.type === "node")
 | 
			
		||||
        for (const node of allNodes) {
 | 
			
		||||
            allNodesById.set(node.id, <OsmNode>node)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        /**
 | 
			
		||||
         * Allright! We know all the nodes of the original way and all the nodes of the target coordinates.
 | 
			
		||||
         * For each of the target coordinates, we search the closest, already existing point and reuse this point
 | 
			
		||||
         */
 | 
			
		||||
 | 
			
		||||
        const closestIds = []
 | 
			
		||||
        const distances = []
 | 
			
		||||
        for (const target of this.targetCoordinates) {
 | 
			
		||||
            let closestDistance = undefined
 | 
			
		||||
            let closestId = undefined;
 | 
			
		||||
            for (const osmNode of allNodes) {
 | 
			
		||||
 | 
			
		||||
                const cp = osmNode.centerpoint()
 | 
			
		||||
                const d = GeoOperations.distanceBetween(target, [cp[1], cp[0]])
 | 
			
		||||
                if (closestId === undefined || closestDistance > d) {
 | 
			
		||||
                    closestId = osmNode.id
 | 
			
		||||
                    closestDistance = d
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            closestIds.push(closestId)
 | 
			
		||||
            distances.push(closestDistance)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Next step: every closestId can only occur once in the list
 | 
			
		||||
        for (let i = 0; i < closestIds.length; i++) {
 | 
			
		||||
            const closestId = closestIds[i]
 | 
			
		||||
            for (let j = i + 1; j < closestIds.length; j++) {
 | 
			
		||||
                const otherClosestId = closestIds[j]
 | 
			
		||||
                if (closestId !== otherClosestId) {
 | 
			
		||||
                    continue
 | 
			
		||||
                }
 | 
			
		||||
                // We have two occurences of 'closestId' - we only keep the closest instance!
 | 
			
		||||
                const di = distances[i]
 | 
			
		||||
                const dj = distances[j]
 | 
			
		||||
                if (di < dj) {
 | 
			
		||||
                    closestIds[j] = undefined
 | 
			
		||||
                } else {
 | 
			
		||||
                    closestIds[i] = undefined
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        const osmWay = <OsmWay>parsed[parsed.length - 1]
 | 
			
		||||
        if (osmWay.type !== "way") {
 | 
			
		||||
            throw "WEIRD: expected an OSM-way as last element here!"
 | 
			
		||||
        }
 | 
			
		||||
        return {closestIds, allNodesById, osmWay};
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -114,7 +114,16 @@ export class Changes {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    public async applyAction(action: OsmChangeAction): Promise<void> {
 | 
			
		||||
        const changes = await action.Perform(this)
 | 
			
		||||
        this.applyChanges(await action.Perform(this))
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async applyActions(actions: OsmChangeAction[]) {
 | 
			
		||||
        for (const action of actions) {
 | 
			
		||||
            await this.applyAction(action)
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public applyChanges(changes: ChangeDescription[]) {
 | 
			
		||||
        console.log("Received changes:", changes)
 | 
			
		||||
        this.pendingChanges.data.push(...changes);
 | 
			
		||||
        this.pendingChanges.ping();
 | 
			
		||||
| 
						 | 
				
			
			@ -126,6 +135,7 @@ export class Changes {
 | 
			
		|||
        CreateNewNodeAction.registerIdRewrites(mappings)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * UPload the selected changes to OSM.
 | 
			
		||||
     * Returns 'true' if successfull and if they can be removed
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -14,6 +14,7 @@ import {QueryParameters} from "../Web/QueryParameters";
 | 
			
		|||
import * as personal from "../../assets/themes/personal/personal.json";
 | 
			
		||||
import FilterConfig from "../../Models/ThemeConfig/FilterConfig";
 | 
			
		||||
import ShowOverlayLayer from "../../UI/ShowDataLayer/ShowOverlayLayer";
 | 
			
		||||
import {Coord} from "@turf/turf";
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Contains all the leaflet-map related state
 | 
			
		||||
| 
						 | 
				
			
			@ -44,13 +45,7 @@ export default class MapState extends UserRelatedState {
 | 
			
		|||
    /**
 | 
			
		||||
     * The location as delivered by the GPS
 | 
			
		||||
     */
 | 
			
		||||
    public currentGPSLocation: UIEventSource<{
 | 
			
		||||
        latlng: { lat: number; lng: number };
 | 
			
		||||
        accuracy: number;
 | 
			
		||||
    }> = new UIEventSource<{
 | 
			
		||||
        latlng: { lat: number; lng: number };
 | 
			
		||||
        accuracy: number;
 | 
			
		||||
    }>(undefined);
 | 
			
		||||
    public currentGPSLocation: UIEventSource<Coordinates> = new UIEventSource<Coordinates>(undefined);
 | 
			
		||||
 | 
			
		||||
    public readonly mainMapObject: BaseUIElement & MinimapObj;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue