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"); | ||||
|  |  | |||
|  | @ -117,6 +117,11 @@ export class BBox { | |||
|         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 { | ||||
| 
 | ||||
|         const latDiff = Math.min(maxIncrease / 2, Math.abs(this.maxLat - this.minLat) * factor) | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -67,4 +140,11 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour | |||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 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; | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										28
									
								
								UI/Base/AsyncLazy.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								UI/Base/AsyncLazy.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,28 @@ | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "./VariableUIElement"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import Loading from "./Loading"; | ||||
| 
 | ||||
| export default class AsyncLazy extends BaseUIElement{ | ||||
|     private readonly _f: () => Promise<BaseUIElement>; | ||||
|      | ||||
|     constructor(f: () => Promise<BaseUIElement>) { | ||||
|         super(); | ||||
|         this._f = f; | ||||
|     } | ||||
|      | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         // The caching of the BaseUIElement will guarantee that _f will only be called once
 | ||||
|          | ||||
|         return new VariableUiElement( | ||||
|             UIEventSource.FromPromise(this._f()).map(el => { | ||||
|                 if(el === undefined){ | ||||
|                     return new Loading() | ||||
|                 } | ||||
|                 return el | ||||
|             }) | ||||
|              | ||||
|         ).ConstructElement() | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -19,6 +19,7 @@ export interface MinimapOptions { | |||
| export interface MinimapObj { | ||||
|     readonly leafletMap: UIEventSource<any>,  | ||||
|     installBounds(factor: number | BBox, showRange?: boolean) : void | ||||
|     TakeScreenshot(): Promise<any>; | ||||
| } | ||||
| 
 | ||||
| export default class Minimap { | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ import {Map} from "leaflet"; | |||
| import Minimap, {MinimapObj, MinimapOptions} from "./Minimap"; | ||||
| import {BBox} from "../../Logic/BBox"; | ||||
| import 'leaflet-polylineoffset' | ||||
| import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; | ||||
| 
 | ||||
| export default class MinimapImplementation extends BaseUIElement implements MinimapObj { | ||||
|     private static _nextId = 0; | ||||
|  | @ -278,4 +279,10 @@ export default class MinimapImplementation extends BaseUIElement implements Mini | |||
| 
 | ||||
|         this.leafletMap.setData(map) | ||||
|     } | ||||
|      | ||||
|     public async TakeScreenshot(){ | ||||
|         const screenshotter = new SimpleMapScreenshoter(); | ||||
|         screenshotter.addTo(this.leafletMap.data); | ||||
|         return await screenshotter.takeScreen('image') | ||||
|     } | ||||
| } | ||||
|  | @ -14,6 +14,9 @@ import Toggle from "../Input/Toggle"; | |||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import UserRelatedState from "../../Logic/State/UserRelatedState"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | ||||
| 
 | ||||
|  | @ -24,7 +27,10 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | |||
|                     layoutToUse: LayoutConfig, | ||||
|                     osmConnection: OsmConnection, | ||||
|                     featureSwitchShareScreen: UIEventSource<boolean>, | ||||
|                     featureSwitchMoreQuests: UIEventSource<boolean> | ||||
|                     featureSwitchMoreQuests: UIEventSource<boolean>, | ||||
|                     locationControl: UIEventSource<Loc>,  | ||||
|                     backgroundLayer: UIEventSource<BaseLayer>,  | ||||
|                     filteredLayers: UIEventSource<FilteredLayer[]> | ||||
|                 } & UserRelatedState) { | ||||
|         const layoutToUse = state.layoutToUse; | ||||
|         super( | ||||
|  | @ -39,7 +45,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | |||
|                                          layoutToUse: LayoutConfig, | ||||
|                                          osmConnection: OsmConnection, | ||||
|                                          featureSwitchShareScreen: UIEventSource<boolean>, | ||||
|                                          featureSwitchMoreQuests: UIEventSource<boolean> | ||||
|                                          featureSwitchMoreQuests: UIEventSource<boolean>, | ||||
|                                          locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]> | ||||
|                                      } & UserRelatedState, | ||||
|                                      isShown: UIEventSource<boolean>): | ||||
|         { header: string | BaseUIElement; content: BaseUIElement }[] { | ||||
|  | @ -77,7 +84,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { | |||
|         layoutToUse: LayoutConfig, | ||||
|         osmConnection: OsmConnection, | ||||
|         featureSwitchShareScreen: UIEventSource<boolean>, | ||||
|         featureSwitchMoreQuests: UIEventSource<boolean> | ||||
|         featureSwitchMoreQuests: UIEventSource<boolean>, | ||||
|         locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]> | ||||
|     } & UserRelatedState, currentTab: UIEventSource<number>, isShown: UIEventSource<boolean>) { | ||||
| 
 | ||||
|         const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown) | ||||
|  |  | |||
|  | @ -9,7 +9,6 @@ import Toggle from "../Input/Toggle"; | |||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import Loading from "../Base/Loading"; | ||||
| import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; | ||||
| import CreateNewWayAction from "../../Logic/Osm/Actions/CreateNewWayAction"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
|  | @ -26,6 +25,13 @@ import SpecialVisualizations, {SpecialVisualization} from "../SpecialVisualizati | |||
| import {FixedUiElement} from "../Base/FixedUiElement"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import Minimap from "../Base/Minimap"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import AllKnownLayers from "../../Customizations/AllKnownLayers"; | ||||
| import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; | ||||
| 
 | ||||
| 
 | ||||
| export interface ImportButtonState { | ||||
|  | @ -38,6 +44,8 @@ export interface ImportButtonState { | |||
|     feature: any, | ||||
|     minZoom: number, | ||||
|     state: { | ||||
|         backgroundLayer: UIEventSource<BaseLayer>; | ||||
|         filteredLayers: UIEventSource<FilteredLayer[]>; | ||||
|         featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|         featurePipeline: FeaturePipeline; | ||||
|         allElements: ElementStorage; | ||||
|  | @ -48,8 +56,14 @@ export interface ImportButtonState { | |||
|         locationControl: UIEventSource<{ zoom: number }> | ||||
|     }, | ||||
|     guiState: { filterViewIsOpened: UIEventSource<boolean> }, | ||||
|     snapToLayers?: string[], | ||||
|     snapToLayersMaxDist?: number | ||||
| 
 | ||||
|     snapSettings?: { | ||||
|         snapToLayers: string[], | ||||
|         snapToLayersMaxDist?: number | ||||
|     }, | ||||
|     conflationSettings?: { | ||||
|         conflateWayId: string | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export class ImportButtonSpecialViz implements SpecialVisualization { | ||||
|  | @ -83,7 +97,7 @@ In the case that MapComplete is pointed to the testing grounds, the edit will be | |||
| 
 | ||||
| #### Specifying which tags to copy or add | ||||
| 
 | ||||
| The first argument of the import button takes a \`;\`-seperated list of tags to add.
 | ||||
| The argument \`tags\` of the import button takes a \`;\`-seperated list of tags to add.
 | ||||
| 
 | ||||
| ${Utils.Special_visualizations_tagsToApplyHelpText} | ||||
| 
 | ||||
|  | @ -113,8 +127,9 @@ ${Utils.Special_visualizations_tagsToApplyHelpText} | |||
|             doc: "How far the contributor must zoom in before being able to import the point", | ||||
|             defaultValue: "18" | ||||
|         }, { | ||||
|             name: "Snap onto layer(s)", | ||||
|             doc: "If a way of the given layer is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list", | ||||
|             name: "Snap onto layer(s)/replace geometry with this other way", | ||||
|             doc: " - If the value corresponding with this key starts with 'way/' and the feature is a LineString or Polygon, the original OSM-way geometry will be changed to match the new geometry\n" + | ||||
|                 " - If a way of the given layer(s) is closeby, will snap the new point onto this way (similar as preset might snap). To show multiple layers to snap onto, use a `;`-seperated list", | ||||
|         }, { | ||||
|             name: "snap max distance", | ||||
|             doc: "The maximum distance that this point will move to snap onto a layer (in meters)", | ||||
|  | @ -130,7 +145,7 @@ ${Utils.Special_visualizations_tagsToApplyHelpText} | |||
|         const id = tagSource.data.id; | ||||
|         const feature = state.allElements.ContainingFeatures.get(id) | ||||
|         let minZoom = args[4] == "" ? 18 : Number(args[4]) | ||||
|         if(isNaN(minZoom)){ | ||||
|         if (isNaN(minZoom)) { | ||||
|             console.warn("Invalid minzoom:", minZoom) | ||||
|             minZoom = 18 | ||||
|         } | ||||
|  | @ -145,13 +160,29 @@ ${Utils.Special_visualizations_tagsToApplyHelpText} | |||
|             img = () => Svg.add_ui() | ||||
|         } | ||||
| 
 | ||||
|         const snapToLayers = args[5]?.split(";").filter(s => s !== "") | ||||
|         const snapToLayersMaxDist = Number(args[6] ?? 6) | ||||
|         let snapSettings = undefined | ||||
|         let conflationSettings = undefined | ||||
|         const possibleWayId = tagSource.data[args[5]] | ||||
|         if (possibleWayId?.startsWith("way/")) { | ||||
|             // This is a conflation
 | ||||
|             conflationSettings = { | ||||
|                 conflateWayId: possibleWayId | ||||
|             } | ||||
|         } else { | ||||
| 
 | ||||
|         if (targetLayer === undefined) { | ||||
|             const e = "Target layer not defined: error in import button for theme: " + state.layoutToUse.id + ": layer " + args[0] + " not found" | ||||
|             console.error(e) | ||||
|             return new FixedUiElement(e).SetClass("alert") | ||||
| 
 | ||||
|             const snapToLayers = args[5]?.split(";").filter(s => s !== "") | ||||
|             const snapToLayersMaxDist = Number(args[6] ?? 6) | ||||
| 
 | ||||
|             if (targetLayer === undefined) { | ||||
|                 const e = "Target layer not defined: error in import button for theme: " + state.layoutToUse.id + ": layer " + args[0] + " not found" | ||||
|                 console.error(e) | ||||
|                 return new FixedUiElement(e).SetClass("alert") | ||||
|             } | ||||
|             snapSettings = { | ||||
|                 snapToLayers, | ||||
|                 snapToLayersMaxDist | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return new ImportButton( | ||||
|  | @ -160,8 +191,8 @@ ${Utils.Special_visualizations_tagsToApplyHelpText} | |||
|                 feature, newTags, message, minZoom, | ||||
|                 originalTags: tagSource, | ||||
|                 targetLayer, | ||||
|                 snapToLayers, | ||||
|                 snapToLayersMaxDist | ||||
|                 snapSettings, | ||||
|                 conflationSettings | ||||
|             } | ||||
|         ); | ||||
|     } | ||||
|  | @ -201,7 +232,7 @@ export default class ImportButton extends Toggle { | |||
| 
 | ||||
|         const importClicked = new UIEventSource(false); | ||||
|         const importFlow = new Toggle( | ||||
|             new Lazy(() => ImportButton.createConfirmPanel(o, isImported, importClicked)), | ||||
|             ImportButton.createConfirmPanel(o, isImported, importClicked), | ||||
|             importButton, | ||||
|             importClicked | ||||
|         ) | ||||
|  | @ -228,7 +259,121 @@ export default class ImportButton extends Toggle { | |||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     public static createConfirmPanel( | ||||
|     public static createConfirmPanel(o: ImportButtonState, | ||||
|                                      isImported: UIEventSource<boolean>, | ||||
|                                      importClicked: UIEventSource<boolean>) { | ||||
|         const geometry = o.feature.geometry | ||||
|         if (geometry.type === "Point") { | ||||
|             return new Lazy(() => ImportButton.createConfirmPanelForPoint(o, isImported, importClicked)) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (geometry.type === "Polygon" || geometry.type == "LineString") { | ||||
|             return new Lazy(() => ImportButton.createConfirmForWay(o, isImported, importClicked)) | ||||
|         } | ||||
|         console.error("Invalid type to import", geometry.type) | ||||
|         return new FixedUiElement("Invalid geometry type:" + geometry.type).SetClass("alert") | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public static createConfirmForWay(o: ImportButtonState, | ||||
|                                             isImported: UIEventSource<boolean>, | ||||
|                                             importClicked: UIEventSource<boolean>): BaseUIElement { | ||||
| 
 | ||||
|         const confirmationMap = Minimap.createMiniMap({ | ||||
|             allowMoving: false, | ||||
|             background: o.state.backgroundLayer | ||||
|         }) | ||||
|         confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") | ||||
| 
 | ||||
|         const relevantFeatures = Utils.NoNull([o.feature, o.state.allElements?.ContainingFeatures?.get(o.conflationSettings?.conflateWayId)]) | ||||
|         // SHow all relevant data - including (eventually) the way of which the geometry will be replaced
 | ||||
|         new ShowDataMultiLayer({ | ||||
|             leafletMap: confirmationMap.leafletMap, | ||||
|             enablePopups: false, | ||||
|             zoomToFeatures: true, | ||||
|             features: new StaticFeatureSource(relevantFeatures, false), | ||||
|             allElements: o.state.allElements, | ||||
|             layers: o.state.filteredLayers | ||||
|         }) | ||||
| 
 | ||||
|         const theme = o.state.layoutToUse.id | ||||
| 
 | ||||
| 
 | ||||
|         const changes = o.state.changes | ||||
|         let confirm: () => Promise<string> | ||||
|         if (o.conflationSettings !== undefined) { | ||||
| 
 | ||||
|             let replaceGeometryAction = new ReplaceGeometryAction( | ||||
|                 o.state, | ||||
|                 o.feature, | ||||
|                 o.conflationSettings.conflateWayId, | ||||
|                 { | ||||
|                     theme: o.state.layoutToUse.id, | ||||
|                     newTags: o.newTags.data | ||||
|                 } | ||||
|             ) | ||||
|              | ||||
|             replaceGeometryAction.GetPreview().then(changePreview => { | ||||
|                 new ShowDataLayer({ | ||||
|                     leafletMap: confirmationMap.leafletMap, | ||||
|                     enablePopups: false, | ||||
|                     zoomToFeatures: false, | ||||
|                     features: changePreview, | ||||
|                     allElements: o.state.allElements, | ||||
|                     layerToShow: AllKnownLayers.sharedLayers.get("conflation") | ||||
|                 }) | ||||
|             }) | ||||
| 
 | ||||
|             confirm = async () => { | ||||
|                 changes.applyAction (replaceGeometryAction) | ||||
|                 return o.feature.properties.id | ||||
|             } | ||||
| 
 | ||||
|         } else { | ||||
|             confirm = async () => { | ||||
|                 const geom = o.feature.geometry | ||||
|                 let coordinates: [number, number][] | ||||
|                 if (geom.type === "LineString") { | ||||
|                     coordinates = geom.coordinates | ||||
|                 } else if (geom.type === "Polygon") { | ||||
|                     coordinates = geom.coordinates[0] | ||||
|                 } | ||||
|                 const action = new CreateNewWayAction(o.newTags.data, coordinates.map(lngLat => ({ | ||||
|                     lat: lngLat[1], | ||||
|                     lon: lngLat[0] | ||||
|                 })), {theme}) | ||||
|                 return action.newElementId | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const confirmButton = new SubtleButton(o.image(), o.message) | ||||
|         confirmButton.onClick(async () => { | ||||
|             { | ||||
|                 if (isImported.data) { | ||||
|                     return | ||||
|                 } | ||||
|                 o.originalTags.data["_imported"] = "yes" | ||||
|                 o.originalTags.ping() // will set isImported as per its definition
 | ||||
| 
 | ||||
|                 const idToSelect = await confirm() | ||||
| 
 | ||||
|                 o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get(idToSelect)) | ||||
| 
 | ||||
|             } | ||||
|         }) | ||||
| 
 | ||||
|         const cancel = new SubtleButton(Svg.close_ui(), Translations.t.general.cancel).onClick(() => { | ||||
|             importClicked.setData(false) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         return new Combine([confirmationMap, confirmButton, cancel]).SetClass("flex flex-col") | ||||
|     } | ||||
| 
 | ||||
|     public static createConfirmPanelForPoint( | ||||
|         o: ImportButtonState, | ||||
|         isImported: UIEventSource<boolean>, | ||||
|         importClicked: UIEventSource<boolean>): BaseUIElement { | ||||
|  | @ -239,39 +384,43 @@ export default class ImportButton extends Toggle { | |||
|             } | ||||
|             o.originalTags.data["_imported"] = "yes" | ||||
|             o.originalTags.ping() // will set isImported as per its definition
 | ||||
|             const newElementAction = ImportButton.createAddActionForFeature(o.newTags.data, o.feature, o.state.layoutToUse.id) | ||||
|             const geometry = o.feature.geometry | ||||
|             const lat = geometry.coordinates[1] | ||||
|             const lon = geometry.coordinates[0]; | ||||
|             const newElementAction = new CreateNewNodeAction(o.newTags.data, lat, lon, { | ||||
|                 theme: o.state.layoutToUse.id, | ||||
|                 changeType: "import" | ||||
|             }) | ||||
| 
 | ||||
|             await o.state.changes.applyAction(newElementAction) | ||||
|             o.state.selectedElement.setData(o.state.allElements.ContainingFeatures.get( | ||||
|                 newElementAction.newElementId | ||||
|             )) | ||||
|             console.log("Did set selected element to", o.state.allElements.ContainingFeatures.get( | ||||
|                 newElementAction.newElementId | ||||
|             )) | ||||
|         } | ||||
| 
 | ||||
|         function cancel() { | ||||
|             importClicked.setData(false) | ||||
|         } | ||||
| 
 | ||||
|         if (o.feature.geometry.type === "Point") { | ||||
|             const presetInfo = <PresetInfo>{ | ||||
|                 tags: o.newTags.data, | ||||
|                 icon: o.image, | ||||
|                 description: o.description, | ||||
|                 layerToAddTo: o.targetLayer, | ||||
|                 name: o.message, | ||||
|                 title: o.message, | ||||
|                 preciseInput: { snapToLayers: o.snapToLayers, | ||||
|                     maxSnapDistance: o.snapToLayersMaxDist} | ||||
|         const presetInfo = <PresetInfo>{ | ||||
|             tags: o.newTags.data, | ||||
|             icon: o.image, | ||||
|             description: o.description, | ||||
|             layerToAddTo: o.targetLayer, | ||||
|             name: o.message, | ||||
|             title: o.message, | ||||
|             preciseInput: { | ||||
|                 snapToLayers: o.snapSettings?.snapToLayers, | ||||
|                 maxSnapDistance: o.snapSettings?.snapToLayersMaxDist | ||||
|             } | ||||
| 
 | ||||
|             const [lon, lat] = o.feature.geometry.coordinates | ||||
|             console.log("Creating an import dialog at location", lon, lat) | ||||
|             return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), { | ||||
|                 lon, | ||||
|                 lat | ||||
|             }, confirm, cancel) | ||||
|         } | ||||
| 
 | ||||
|         const [lon, lat] = o.feature.geometry.coordinates | ||||
|         return new ConfirmLocationOfPoint(o.state, o.guiState.filterViewIsOpened, presetInfo, Translations.W(o.message), { | ||||
|             lon, | ||||
|             lat | ||||
|         }, confirm, cancel) | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|  | @ -279,41 +428,4 @@ export default class ImportButton extends Toggle { | |||
|         const type = feature.geometry.type | ||||
|         return type === "Point" || type === "LineString" || (type === "Polygon" && feature.geometry.coordinates.length === 1) | ||||
|     } | ||||
| 
 | ||||
|     private static createAddActionForFeature(newTags: Tag[], feature: any, theme: string): | ||||
|         OsmChangeAction & { newElementId: string } { | ||||
|         const geometry = feature.geometry | ||||
|         const type = geometry.type | ||||
|         if (type === "Point") { | ||||
|             const lat = geometry.coordinates[1] | ||||
|             const lon = geometry.coordinates[0]; | ||||
|             return new CreateNewNodeAction(newTags, lat, lon, { | ||||
|                 theme, | ||||
|                 changeType: "import" | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         if (type === "LineString") { | ||||
|             return new CreateNewWayAction( | ||||
|                 newTags, | ||||
|                 geometry.coordinates.map(coor => ({lon: coor[0], lat: coor[1]})), | ||||
|                 { | ||||
|                     theme | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         if (type === "Polygon") { | ||||
|             return new CreateNewWayAction( | ||||
|                 newTags, | ||||
|                 geometry.coordinates[0].map(coor => ({lon: coor[0], lat: coor[1]})), | ||||
|                 { | ||||
|                     theme | ||||
|                 } | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|         return undefined; | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -14,6 +14,8 @@ import Loc from "../../Models/Loc"; | |||
| import {BBox} from "../../Logic/BBox"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||
| 
 | ||||
| export default class LeftControls extends Combine { | ||||
| 
 | ||||
|  | @ -26,7 +28,9 @@ export default class LeftControls extends Combine { | |||
|                     featureSwitchEnableExport: UIEventSource<boolean>, | ||||
|                     featureSwitchExportAsPdf: UIEventSource<boolean>, | ||||
|                     filteredLayers: UIEventSource<FilteredLayer[]>, | ||||
|                     featureSwitchFilter: UIEventSource<boolean> | ||||
|                     featureSwitchFilter: UIEventSource<boolean>, | ||||
|                     backgroundLayer: UIEventSource<BaseLayer>, | ||||
|                     osmConnection: OsmConnection | ||||
|                 }, | ||||
|                 guiState: { | ||||
|                     downloadControlIsOpened: UIEventSource<boolean>, | ||||
|  |  | |||
|  | @ -4,17 +4,30 @@ import MapControlButton from "../MapControlButton"; | |||
| import GeoLocationHandler from "../../Logic/Actors/GeoLocationHandler"; | ||||
| import Svg from "../../Svg"; | ||||
| import MapState from "../../Logic/State/MapState"; | ||||
| import ShowDataLayer from "../ShowDataLayer/ShowDataLayer"; | ||||
| import AllKnownLayers from "../../Customizations/AllKnownLayers"; | ||||
| 
 | ||||
| export default class RightControls extends Combine { | ||||
| 
 | ||||
|     constructor(state:MapState) { | ||||
|          | ||||
|         const geolocatioHandler = new GeoLocationHandler( | ||||
|             state.currentGPSLocation, | ||||
|             state.leafletMap, | ||||
|             state.layoutToUse | ||||
|         ) | ||||
|          | ||||
|         new ShowDataLayer({ | ||||
|             layerToShow: AllKnownLayers.sharedLayers.get("gps_location"), | ||||
|             leafletMap: state.leafletMap, | ||||
|             enablePopups: true, | ||||
|             features: geolocatioHandler.currentLocation | ||||
|         }) | ||||
|          | ||||
|         const geolocationButton = new Toggle( | ||||
|             new MapControlButton( | ||||
|                 new GeoLocationHandler( | ||||
|                     state.currentGPSLocation, | ||||
|                     state.leafletMap, | ||||
|                     state.layoutToUse | ||||
|                 ), { | ||||
|                 geolocatioHandler | ||||
|                 , { | ||||
|                     dontStyle: true | ||||
|                 } | ||||
|             ), | ||||
|  |  | |||
|  | @ -8,11 +8,14 @@ import Toggle from "../Input/Toggle"; | |||
| import Translations from "../i18n/Translations"; | ||||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import LayerConfig from "../../Models/ThemeConfig/LayerConfig"; | ||||
| import MapState from "../../Logic/State/MapState"; | ||||
| import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import BaseLayer from "../../Models/BaseLayer"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| 
 | ||||
| export default class ShareScreen extends Combine { | ||||
| 
 | ||||
|     constructor(state: MapState) { | ||||
|     constructor(state: {layoutToUse: LayoutConfig, locationControl: UIEventSource<Loc>, backgroundLayer: UIEventSource<BaseLayer>, filteredLayers: UIEventSource<FilteredLayer[]>}) { | ||||
|         const layout = state?.layoutToUse; | ||||
|         const tr = Translations.t.general.sharescreen; | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,9 +6,6 @@ import FullWelcomePaneWithTabs from "./BigComponents/FullWelcomePaneWithTabs"; | |||
| import MapControlButton from "./MapControlButton"; | ||||
| import Svg from "../Svg"; | ||||
| import Toggle from "./Input/Toggle"; | ||||
| import Hash from "../Logic/Web/Hash"; | ||||
| import {QueryParameters} from "../Logic/Web/QueryParameters"; | ||||
| import Constants from "../Models/Constants"; | ||||
| import UserBadge from "./BigComponents/UserBadge"; | ||||
| import SearchAndGo from "./BigComponents/SearchAndGo"; | ||||
| import Link from "./Base/Link"; | ||||
|  | @ -24,77 +21,7 @@ import Translations from "./i18n/Translations"; | |||
| import SimpleAddUI from "./BigComponents/SimpleAddUI"; | ||||
| import StrayClickHandler from "../Logic/Actors/StrayClickHandler"; | ||||
| import Lazy from "./Base/Lazy"; | ||||
| 
 | ||||
| export class DefaultGuiState { | ||||
|     public readonly welcomeMessageIsOpened : UIEventSource<boolean>; | ||||
|     public readonly downloadControlIsOpened: UIEventSource<boolean>; | ||||
|     public readonly filterViewIsOpened: UIEventSource<boolean>; | ||||
|     public readonly copyrightViewIsOpened: UIEventSource<boolean>; | ||||
|     public readonly welcomeMessageOpenedTab: UIEventSource<number> | ||||
|     public readonly allFullScreenStates: UIEventSource<boolean>[] = [] | ||||
|     static state: DefaultGuiState; | ||||
| 
 | ||||
|     constructor() { | ||||
| 
 | ||||
|          | ||||
| 
 | ||||
|         this.welcomeMessageOpenedTab = UIEventSource.asFloat(QueryParameters.GetQueryParameter( | ||||
|             "tab", | ||||
|             "0", | ||||
|             `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)` | ||||
|         )); | ||||
|         this.welcomeMessageIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "welcome-control-toggle", | ||||
|             "false", | ||||
|             "Whether or not the welcome panel is shown" | ||||
|         ) | ||||
|         this.downloadControlIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|                 "download-control-toggle", | ||||
|                 "false", | ||||
|                 "Whether or not the download panel is shown" | ||||
|             ) | ||||
|         this.filterViewIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "filter-toggle", | ||||
|             "false", | ||||
|             "Whether or not the filter view is shown" | ||||
|         ) | ||||
|         this.copyrightViewIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "copyright-toggle", | ||||
|             "false", | ||||
|             "Whether or not the copyright view is shown" | ||||
|         ) | ||||
|         if(Hash.hash.data === "download"){ | ||||
|             this.downloadControlIsOpened.setData(true) | ||||
|         } | ||||
|         if(Hash.hash.data === "filters"){ | ||||
|             this.filterViewIsOpened.setData(true) | ||||
|         } | ||||
|         if(Hash.hash.data === "copyright"){ | ||||
|             this.copyrightViewIsOpened.setData(true) | ||||
|         } | ||||
|         if(Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome"){ | ||||
|             this.welcomeMessageIsOpened.setData(true) | ||||
|         } | ||||
|          | ||||
|         this.allFullScreenStates.push(this.downloadControlIsOpened, this.filterViewIsOpened, this.copyrightViewIsOpened, this.welcomeMessageIsOpened) | ||||
| 
 | ||||
|         for (let i = 0; i < this.allFullScreenStates.length; i++){ | ||||
|             const fullScreenState = this.allFullScreenStates[i]; | ||||
|             for (let j = 0; j < this.allFullScreenStates.length; j++){ | ||||
|                 if(i == j){ | ||||
|                     continue | ||||
|                 } | ||||
|                 const otherState = this.allFullScreenStates[j]; | ||||
|                 fullScreenState.addCallbackAndRunD(isOpened => { | ||||
|                     if(isOpened){ | ||||
|                         otherState.setData(false) | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
| import {DefaultGuiState} from "./DefaultGuiState"; | ||||
| 
 | ||||
| 
 | ||||
| /** | ||||
|  |  | |||
							
								
								
									
										74
									
								
								UI/DefaultGuiState.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								UI/DefaultGuiState.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,74 @@ | |||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import {QueryParameters} from "../Logic/Web/QueryParameters"; | ||||
| import Constants from "../Models/Constants"; | ||||
| import Hash from "../Logic/Web/Hash"; | ||||
| 
 | ||||
| export class DefaultGuiState { | ||||
|     public readonly welcomeMessageIsOpened: UIEventSource<boolean>; | ||||
|     public readonly downloadControlIsOpened: UIEventSource<boolean>; | ||||
|     public readonly filterViewIsOpened: UIEventSource<boolean>; | ||||
|     public readonly copyrightViewIsOpened: UIEventSource<boolean>; | ||||
|     public readonly welcomeMessageOpenedTab: UIEventSource<number> | ||||
|     public readonly allFullScreenStates: UIEventSource<boolean>[] = [] | ||||
|     static state: DefaultGuiState; | ||||
| 
 | ||||
|     constructor() { | ||||
| 
 | ||||
| 
 | ||||
|         this.welcomeMessageOpenedTab = UIEventSource.asFloat(QueryParameters.GetQueryParameter( | ||||
|             "tab", | ||||
|             "0", | ||||
|             `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${Constants.userJourney.mapCompleteHelpUnlock} changesets)` | ||||
|         )); | ||||
|         this.welcomeMessageIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "welcome-control-toggle", | ||||
|             "false", | ||||
|             "Whether or not the welcome panel is shown" | ||||
|         ) | ||||
|         this.downloadControlIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "download-control-toggle", | ||||
|             "false", | ||||
|             "Whether or not the download panel is shown" | ||||
|         ) | ||||
|         this.filterViewIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "filter-toggle", | ||||
|             "false", | ||||
|             "Whether or not the filter view is shown" | ||||
|         ) | ||||
|         this.copyrightViewIsOpened = QueryParameters.GetBooleanQueryParameter( | ||||
|             "copyright-toggle", | ||||
|             "false", | ||||
|             "Whether or not the copyright view is shown" | ||||
|         ) | ||||
|         if (Hash.hash.data === "download") { | ||||
|             this.downloadControlIsOpened.setData(true) | ||||
|         } | ||||
|         if (Hash.hash.data === "filters") { | ||||
|             this.filterViewIsOpened.setData(true) | ||||
|         } | ||||
|         if (Hash.hash.data === "copyright") { | ||||
|             this.copyrightViewIsOpened.setData(true) | ||||
|         } | ||||
|         if (Hash.hash.data === "" || Hash.hash.data === undefined || Hash.hash.data === "welcome") { | ||||
|             this.welcomeMessageIsOpened.setData(true) | ||||
|         } | ||||
| 
 | ||||
|         this.allFullScreenStates.push(this.downloadControlIsOpened, this.filterViewIsOpened, this.copyrightViewIsOpened, this.welcomeMessageIsOpened) | ||||
| 
 | ||||
|         for (let i = 0; i < this.allFullScreenStates.length; i++) { | ||||
|             const fullScreenState = this.allFullScreenStates[i]; | ||||
|             for (let j = 0; j < this.allFullScreenStates.length; j++) { | ||||
|                 if (i == j) { | ||||
|                     continue | ||||
|                 } | ||||
|                 const otherState = this.allFullScreenStates[j]; | ||||
|                 fullScreenState.addCallbackAndRunD(isOpened => { | ||||
|                     if (isOpened) { | ||||
|                         otherState.setData(false) | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -1,9 +1,6 @@ | |||
| 
 | ||||
| 
 | ||||
| import jsPDF from "jspdf"; | ||||
| import {SimpleMapScreenshoter} from "leaflet-simple-map-screenshoter"; | ||||
| import {UIEventSource} from "../Logic/UIEventSource"; | ||||
| import Minimap from "./Base/Minimap"; | ||||
| import Minimap, {MinimapObj} from "./Base/Minimap"; | ||||
| import Loc from "../Models/Loc"; | ||||
| import BaseLayer from "../Models/BaseLayer"; | ||||
| import {FixedUiElement} from "./Base/FixedUiElement"; | ||||
|  | @ -14,7 +11,6 @@ import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | |||
| import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; | ||||
| import ShowDataLayer from "./ShowDataLayer/ShowDataLayer"; | ||||
| import {BBox} from "../Logic/BBox"; | ||||
| import ShowOverlayLayer from "./ShowDataLayer/ShowOverlayLayer"; | ||||
| /** | ||||
|  * Creates screenshoter to take png screenshot | ||||
|  * Creates jspdf and downloads it | ||||
|  | @ -63,14 +59,12 @@ export default class ExportPDF { | |||
|             location: new UIEventSource<Loc>(loc), // We remove the link between the old and the new UI-event source as moving the map while the export is running fucks up the screenshot
 | ||||
|             background: options.background, | ||||
|             allowMoving: false, | ||||
| 
 | ||||
| 
 | ||||
|             onFullyLoaded: leaflet => window.setTimeout(() => { | ||||
|             onFullyLoaded: _ => window.setTimeout(() => { | ||||
|                 if (self._screenhotTaken) { | ||||
|                     return; | ||||
|                 } | ||||
|                 try { | ||||
|                     self.CreatePdf(leaflet) | ||||
|                     self.CreatePdf(minimap) | ||||
|                         .then(() => self.cleanup()) | ||||
|                         .catch(() => self.cleanup()) | ||||
|                 } catch (e) { | ||||
|  | @ -112,20 +106,17 @@ export default class ExportPDF { | |||
|         this._screenhotTaken = true; | ||||
|     } | ||||
| 
 | ||||
|     private async CreatePdf(leaflet: L.Map) { | ||||
|     private async CreatePdf(minimap: MinimapObj) { | ||||
|          | ||||
|          | ||||
|          | ||||
|         console.log("PDF creation started") | ||||
|         const t = Translations.t.general.pdf; | ||||
|         const layout = this._layout | ||||
|         const screenshotter = new SimpleMapScreenshoter(); | ||||
|         //minimap op index.html -> hidden daar alles op doen en dan weg
 | ||||
|         //minimap - leaflet map ophalen - boundaries ophalen - State.state.featurePipeline
 | ||||
|         screenshotter.addTo(leaflet); | ||||
| 
 | ||||
| 
 | ||||
|         let doc = new jsPDF('landscape'); | ||||
| 
 | ||||
| 
 | ||||
|         const image = (await screenshotter.takeScreen('image')) | ||||
|         const image = await minimap.TakeScreenshot() | ||||
|         // @ts-ignore
 | ||||
|         doc.addImage(image, 'PNG', 0, 0, this.mapW, this.mapH); | ||||
| 
 | ||||
|  |  | |||
|  | @ -167,6 +167,9 @@ export default class LocationInput extends InputElement<Loc> implements MinimapO | |||
|     installBounds(factor: number | BBox, showRange?: boolean): void { | ||||
|         this.map.installBounds(factor, showRange) | ||||
|     } | ||||
|     TakeScreenshot(): Promise<any> { | ||||
|        return this.map.TakeScreenshot() | ||||
|     } | ||||
| 
 | ||||
|     protected InnerConstructElement(): HTMLElement { | ||||
|         try { | ||||
|  |  | |||
|  | @ -58,7 +58,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|             for (const groupName of allGroupNames) { | ||||
|                 const questions = layerConfig.tagRenderings.filter(tr => tr.group === groupName) | ||||
|                 const questionBox = new QuestionBox(tags, questions, layerConfig.units); | ||||
|                 console.log("Groupname:", groupName) | ||||
|                 questionBoxes.set(groupName, questionBox) | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -155,7 +155,6 @@ export default class ShowDataLayer { | |||
|                 continue | ||||
|             } | ||||
|             try { | ||||
| 
 | ||||
|                 if ((feat.geometry.type === "LineString" || feat.geometry.type === "MultiLineString")) { | ||||
|                     const self = this; | ||||
|                     const coords = L.GeoJSON.coordsToLatLngs(feat.geometry.coordinates) | ||||
|  | @ -190,9 +189,10 @@ export default class ShowDataLayer { | |||
| 
 | ||||
|         if (options.zoomToFeatures ?? false) { | ||||
|             try { | ||||
|                 mp.fitBounds(this.geoLayer.getBounds(), {animate: false}) | ||||
|                 const bounds = this.geoLayer.getBounds() | ||||
|                 mp.fitBounds(bounds, {animate: false}) | ||||
|             } catch (e) { | ||||
|                 console.error(e) | ||||
|                 console.debug("Invalid bounds",e) | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -20,7 +20,7 @@ import Histogram from "./BigComponents/Histogram"; | |||
| import Loc from "../Models/Loc"; | ||||
| import {Utils} from "../Utils"; | ||||
| import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||
| import ImportButton, {ImportButtonSpecialViz} from "./BigComponents/ImportButton"; | ||||
| import {ImportButtonSpecialViz} from "./BigComponents/ImportButton"; | ||||
| import {Tag} from "../Logic/Tags/Tag"; | ||||
| import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import ShowDataMultiLayer from "./ShowDataLayer/ShowDataMultiLayer"; | ||||
|  | @ -38,9 +38,9 @@ import {SubtleButton} from "./Base/SubtleButton"; | |||
| import ChangeTagAction from "../Logic/Osm/Actions/ChangeTagAction"; | ||||
| import {And} from "../Logic/Tags/And"; | ||||
| import Toggle from "./Input/Toggle"; | ||||
| import {DefaultGuiState} from "./DefaultGUI"; | ||||
| import Img from "./Base/Img"; | ||||
| import FilteredLayer from "../Models/FilteredLayer"; | ||||
| import {DefaultGuiState} from "./DefaultGuiState"; | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|     funcName: string, | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ import {Utils} from "../Utils"; | |||
| import {VariableUiElement} from "./Base/VariableUIElement"; | ||||
| import Combine from "./Base/Combine"; | ||||
| import BaseUIElement from "./BaseUIElement"; | ||||
| import {DefaultGuiState} from "./DefaultGUI"; | ||||
| import {DefaultGuiState} from "./DefaultGuiState"; | ||||
| 
 | ||||
| export class SubstitutedTranslation extends VariableUiElement { | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										32
									
								
								assets/layers/conflation/conflation.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								assets/layers/conflation/conflation.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| { | ||||
|   "id": "conflation", | ||||
|   "description": "This is a special meta_layer which render geometry-changes for inspection", | ||||
|   "minzoom": 1, | ||||
|   "source": { | ||||
|     "osmTags": { | ||||
|       "or": ["move=yes","newpoint=yes"] | ||||
|     } | ||||
|   }, | ||||
|   "name": "Conflation", | ||||
|   "title": "Conflation", | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "location": "point", | ||||
|       "icon": "addSmall:#000", | ||||
|       "iconSize": "10,10,center" | ||||
|     }, | ||||
|     { | ||||
|       "location": "end", | ||||
|       "icon": "circle:#0f0", | ||||
|       "iconSize": "10,10,center" | ||||
|     },{ | ||||
|       "location": "start", | ||||
|       "icon": "square:#f00", | ||||
|       "iconSize": "10,10,center" | ||||
|     }, | ||||
|     { | ||||
|       "width": "3", | ||||
|       "color": "#00f" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										15
									
								
								assets/layers/gps_location/gps_location.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								assets/layers/gps_location/gps_location.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| { | ||||
|   "id": "gps_location", | ||||
|   "description": "Meta layer showing the current location of the user", | ||||
|   "minzoom": 0, | ||||
|   "source": { | ||||
|     "osmTags": "user:location=yes" | ||||
|   }, | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": "crosshair:#00f", | ||||
|       "iconSize": "40,40,center", | ||||
|       "location": "point" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -1,35 +1,19 @@ | |||
| { | ||||
|     "id": "home_location", | ||||
|     "description": "Meta layer showing the home location of the user", | ||||
|     "minzoom": 0, | ||||
|     "source": { | ||||
|         "osmTags": "user:home=yes" | ||||
|     }, | ||||
|     "icon": { | ||||
|   "id": "home_location", | ||||
|   "description": "Meta layer showing the home location of the user", | ||||
|   "minzoom": 0, | ||||
|   "source": { | ||||
|     "osmTags": "user:home=yes" | ||||
|   }, | ||||
|   "mapRendering": [ | ||||
|     { | ||||
|       "icon": { | ||||
|         "render": "circle:white;./assets/svg/home.svg" | ||||
|     }, | ||||
|     "iconSize": { | ||||
|       }, | ||||
|       "iconSize": { | ||||
|         "render": "20,20,center" | ||||
|     }, | ||||
|     "color": { | ||||
|         "render": "#00f" | ||||
|     }, | ||||
|     "mapRendering": [ | ||||
|         { | ||||
|             "icon": { | ||||
|                 "render": "circle:white;./assets/svg/home.svg" | ||||
|             }, | ||||
|             "iconSize": { | ||||
|                 "render": "20,20,center" | ||||
|             }, | ||||
|             "location": [ | ||||
|                 "point" | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "color": { | ||||
|                 "render": "#00f" | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|       }, | ||||
|       "location": "point" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -235,11 +235,6 @@ | |||
|         } | ||||
|       ], | ||||
|       "mapRendering": [ | ||||
|         { | ||||
|           "location": [ | ||||
|             "point" | ||||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "color": { | ||||
|             "render": "#ff7392", | ||||
|  |  | |||
|  | @ -38,6 +38,8 @@ | |||
|       "override": { | ||||
|         "calculatedTags": [ | ||||
|           "_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false", | ||||
|           "_is_part_of_building_passage=feat.get('parent_ways')?.some(p => p.tunnel === 'building_passage') ?? false", | ||||
|           "_is_part_of_highway=!feat.get('is_part_of_building_passage') && (feat.get('parent_ways')?.some(p => p.highway !== undefined && p.highway !== '') ?? false)", | ||||
|           "_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? false"  | ||||
|         ] | ||||
|       } | ||||
|  | @ -674,7 +676,7 @@ | |||
|           "mappings": [ | ||||
|             { | ||||
|               "if": "_overlaps_with!=null", | ||||
|               "then": "Cannot be imported directly, there is a nearly identical building geometry in OpenStreetMap" | ||||
|               "then": "{import_button(OSM-buildings,building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,,_osm_obj:id)}" | ||||
|             } | ||||
|           ] | ||||
|         }, | ||||
|  |  | |||
							
								
								
									
										3
									
								
								index.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								index.ts
									
										
									
									
									
								
							|  | @ -9,10 +9,11 @@ import {Utils} from "./Utils"; | |||
| import AllThemesGui from "./UI/AllThemesGui"; | ||||
| import DetermineLayout from "./Logic/DetermineLayout"; | ||||
| import LayoutConfig from "./Models/ThemeConfig/LayoutConfig"; | ||||
| import DefaultGUI, {DefaultGuiState} from "./UI/DefaultGUI"; | ||||
| import DefaultGUI from "./UI/DefaultGUI"; | ||||
| import State from "./State"; | ||||
| import AvailableBaseLayersImplementation from "./Logic/Actors/AvailableBaseLayersImplementation"; | ||||
| import ShowOverlayLayerImplementation from "./UI/ShowDataLayer/ShowOverlayLayerImplementation"; | ||||
| import {DefaultGuiState} from "./UI/DefaultGuiState"; | ||||
| 
 | ||||
| // Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts running from console
 | ||||
| MinimapImplementation.initialize() | ||||
|  |  | |||
							
								
								
									
										163
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										163
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,139 +1,26 @@ | |||
| import {Utils} from "./Utils"; | ||||
| import FullNodeDatabaseSource from "./Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; | ||||
| 
 | ||||
| 
 | ||||
| const data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + | ||||
|     "<osm version=\"0.6\" generator=\"CGImap 0.8.5 (2610109 spike-08.openstreetmap.org)\" copyright=\"OpenStreetMap and contributors\" attribution=\"http://www.openstreetmap.org/copyright\" license=\"http://opendatacommons.org/licenses/odbl/1-0/\">\n" + | ||||
|     " <bounds minlat=\"51.2154864\" minlon=\"3.2176208\" maxlat=\"51.2163466\" maxlon=\"3.2189941\"/>\n" + | ||||
|     " <node id=\"315739208\" visible=\"true\" version=\"6\" changeset=\"11063832\" timestamp=\"2012-03-22T15:12:47Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2149470\" lon=\"3.2183010\"/>\n" + | ||||
|     " <node id=\"315739215\" visible=\"true\" version=\"8\" changeset=\"7350988\" timestamp=\"2011-02-21T09:50:46Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2160281\" lon=\"3.2174966\"/>\n" + | ||||
|     " <node id=\"315739216\" visible=\"true\" version=\"7\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:28Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2152977\" lon=\"3.2195995\"/>\n" + | ||||
|     " <node id=\"315739242\" visible=\"true\" version=\"2\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:29Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2164491\" lon=\"3.2187218\"/>\n" + | ||||
|     " <node id=\"315739243\" visible=\"true\" version=\"9\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:29Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2166162\" lon=\"3.2182807\"/>\n" + | ||||
|     " <node id=\"1682824800\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154405\" lon=\"3.2193489\"/>\n" + | ||||
|     " <node id=\"1682824805\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154354\" lon=\"3.2193255\"/>\n" + | ||||
|     " <node id=\"1682824813\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155117\" lon=\"3.2192071\"/>\n" + | ||||
|     " <node id=\"1682824815\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154693\" lon=\"3.2193015\"/>\n" + | ||||
|     " <node id=\"1682824817\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153821\" lon=\"3.2193628\"/>\n" + | ||||
|     " <node id=\"1682832761\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159858\" lon=\"3.2190646\"/>\n" + | ||||
|     " <node id=\"1682832762\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157564\" lon=\"3.2191917\"/>\n" + | ||||
|     " <node id=\"1682832764\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157971\" lon=\"3.2187018\"/>\n" + | ||||
|     " <node id=\"1682832775\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153841\" lon=\"3.2186735\"/>\n" + | ||||
|     " <node id=\"1682832777\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156194\" lon=\"3.2188368\"/>\n" + | ||||
|     " <node id=\"1682832780\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153013\" lon=\"3.2188337\"/>\n" + | ||||
|     " <node id=\"1682832782\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157716\" lon=\"3.2190346\"/>\n" + | ||||
|     " <node id=\"1682832784\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153225\" lon=\"3.2189135\"/>\n" + | ||||
|     " <node id=\"1682832786\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154508\" lon=\"3.2187894\"/>\n" + | ||||
|     " <node id=\"1682832789\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159305\" lon=\"3.2188530\"/>\n" + | ||||
|     " <node id=\"1682832791\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157323\" lon=\"3.2180624\"/>\n" + | ||||
|     " <node id=\"1682832793\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:21Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159622\" lon=\"3.2188025\"/>\n" + | ||||
|     " <node id=\"1682832795\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159076\" lon=\"3.2189725\"/>\n" + | ||||
|     " <node id=\"1682832804\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157453\" lon=\"3.2186489\"/>\n" + | ||||
|     " <node id=\"1682832808\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157935\" lon=\"3.2186116\"/>\n" + | ||||
|     " <node id=\"1682832813\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2152199\" lon=\"3.2186903\"/>\n" + | ||||
|     " <node id=\"1682832815\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160796\" lon=\"3.2189893\"/>\n" + | ||||
|     " <node id=\"1682832817\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154484\" lon=\"3.2188760\"/>\n" + | ||||
|     " <node id=\"1682832818\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2152395\" lon=\"3.2187686\"/>\n" + | ||||
|     " <node id=\"1682832819\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153621\" lon=\"3.2185962\"/>\n" + | ||||
|     " <node id=\"1682832820\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155109\" lon=\"3.2184903\"/>\n" + | ||||
|     " <node id=\"1682832821\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155922\" lon=\"3.2187693\"/>\n" + | ||||
|     " <node id=\"1682832825\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154945\" lon=\"3.2191530\"/>\n" + | ||||
|     " <node id=\"1682832826\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157122\" lon=\"3.2192275\"/>\n" + | ||||
|     " <node id=\"1682832827\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154505\" lon=\"3.2187683\"/>\n" + | ||||
|     " <node id=\"1682832828\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160312\" lon=\"3.2188379\"/>\n" + | ||||
|     " <node id=\"1682832829\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160071\" lon=\"3.2188563\"/>\n" + | ||||
|     " <node id=\"1682832830\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158432\" lon=\"3.2189346\"/>\n" + | ||||
|     " <node id=\"1682832831\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157027\" lon=\"3.2184476\"/>\n" + | ||||
|     " <node id=\"1682832832\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159424\" lon=\"3.2189018\"/>\n" + | ||||
|     " <node id=\"1682832833\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:22Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156684\" lon=\"3.2191183\"/>\n" + | ||||
|     " <node id=\"1682832834\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158295\" lon=\"3.2185829\"/>\n" + | ||||
|     " <node id=\"1682832835\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156820\" lon=\"3.2180867\"/>\n" + | ||||
|     " <node id=\"1682832836\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155219\" lon=\"3.2191313\"/>\n" + | ||||
|     " <node id=\"1682832837\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158056\" lon=\"3.2190810\"/>\n" + | ||||
|     " <node id=\"1682832838\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157585\" lon=\"3.2190453\"/>\n" + | ||||
|     " <node id=\"1682832839\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153634\" lon=\"3.2186009\"/>\n" + | ||||
|     " <node id=\"1682832840\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157257\" lon=\"3.2185567\"/>\n" + | ||||
|     " <node id=\"1682832841\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159207\" lon=\"3.2190210\"/>\n" + | ||||
|     " <node id=\"1682832842\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160295\" lon=\"3.2190298\"/>\n" + | ||||
|     " <node id=\"1682832843\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156835\" lon=\"3.2183577\"/>\n" + | ||||
|     " <node id=\"1682832846\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158946\" lon=\"3.2189083\"/>\n" + | ||||
|     " <node id=\"1682832848\" visible=\"true\" version=\"1\" changeset=\"11037951\" timestamp=\"2012-03-20T06:38:23Z\" user=\"zors1843\" uid=\"233248\" lat=\"51.2162257\" lon=\"3.2189152\"/>\n" + | ||||
|     " <node id=\"1682832850\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156455\" lon=\"3.2189033\"/>\n" + | ||||
|     " <node id=\"1682832851\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157814\" lon=\"3.2191010\"/>\n" + | ||||
|     " <node id=\"1682832852\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158297\" lon=\"3.2191463\"/>\n" + | ||||
|     " <node id=\"1682832853\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2153884\" lon=\"3.2186848\"/>\n" + | ||||
|     " <node id=\"1682832854\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155190\" lon=\"3.2191206\"/>\n" + | ||||
|     " <node id=\"1682832856\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156298\" lon=\"3.2190100\"/>\n" + | ||||
|     " <node id=\"1682832858\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156874\" lon=\"3.2190086\"/>\n" + | ||||
|     " <node id=\"1682832859\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159818\" lon=\"3.2188688\"/>\n" + | ||||
|     " <node id=\"1682832860\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159413\" lon=\"3.2190997\"/>\n" + | ||||
|     " <node id=\"1682832861\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159831\" lon=\"3.2187874\"/>\n" + | ||||
|     " <node id=\"1682832862\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:23Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157432\" lon=\"3.2189602\"/>\n" + | ||||
|     " <node id=\"1682837112\" visible=\"true\" version=\"3\" changeset=\"113131738\" timestamp=\"2021-10-29T16:20:51Z\" user=\"Pieter Vander Vennet\" uid=\"3818858\" lat=\"51.2160077\" lon=\"3.2187651\"/>\n" + | ||||
|     " <node id=\"1682837113\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161667\" lon=\"3.2187271\"/>\n" + | ||||
|     " <node id=\"1682837114\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162728\" lon=\"3.2185965\"/>\n" + | ||||
|     " <node id=\"1682837115\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161349\" lon=\"3.2185984\"/>\n" + | ||||
|     " <node id=\"1682837120\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163153\" lon=\"3.2187451\"/>\n" + | ||||
|     " <node id=\"1682837122\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2164091\" lon=\"3.2186697\"/>\n" + | ||||
|     " <node id=\"1682837124\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161535\" lon=\"3.2183166\"/>\n" + | ||||
|     " <node id=\"1682837126\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162789\" lon=\"3.2187745\"/>\n" + | ||||
|     " <node id=\"1682837128\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161929\" lon=\"3.2185504\"/>\n" + | ||||
|     " <node id=\"1682837130\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162296\" lon=\"3.2186760\"/>\n" + | ||||
|     " <node id=\"1682837132\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2165327\" lon=\"3.2183323\"/>\n" + | ||||
|     " <node id=\"1682837133\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163805\" lon=\"3.2186936\"/>\n" + | ||||
|     " <node id=\"1682837134\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162456\" lon=\"3.2186644\"/>\n" + | ||||
|     " <node id=\"1682837135\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162404\" lon=\"3.2188053\"/>\n" + | ||||
|     " <node id=\"1682837136\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162428\" lon=\"3.2184412\"/>\n" + | ||||
|     " <node id=\"1682837138\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162938\" lon=\"3.2185770\"/>\n" + | ||||
|     " <node id=\"1682837142\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161321\" lon=\"3.2183620\"/>\n" + | ||||
|     " <node id=\"1682837143\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161991\" lon=\"3.2188378\"/>\n" + | ||||
|     " <node id=\"1682837145\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160656\" lon=\"3.2189444\"/>\n" + | ||||
|     " <node id=\"1682837146\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163975\" lon=\"3.2181513\"/>\n" + | ||||
|     " <node id=\"1682837147\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161668\" lon=\"3.2185723\"/>\n" + | ||||
|     " <node id=\"1682837148\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161842\" lon=\"3.2187118\"/>\n" + | ||||
|     " <node id=\"1682837149\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160893\" lon=\"3.2186362\"/>\n" + | ||||
|     " <node id=\"1682837150\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161619\" lon=\"3.2188670\"/>\n" + | ||||
|     " <node id=\"1682874140\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163477\" lon=\"3.2191243\"/>\n" + | ||||
|     " <node id=\"1682874141\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160579\" lon=\"3.2190935\"/>\n" + | ||||
|     " <node id=\"1682874142\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161979\" lon=\"3.2193991\"/>\n" + | ||||
|     " <node id=\"1682874143\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2162760\" lon=\"3.2189217\"/>\n" + | ||||
|     " <node id=\"1682874144\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163468\" lon=\"3.2188607\"/>\n" + | ||||
|     " <node id=\"1682874150\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163835\" lon=\"3.2190937\"/>\n" + | ||||
|     " <node id=\"1682874151\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:32Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2164206\" lon=\"3.2190633\"/>\n" + | ||||
|     " <node id=\"1682874161\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2161339\" lon=\"3.2194350\"/>\n" + | ||||
|     " <node id=\"1682874164\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163293\" lon=\"3.2191391\"/>\n" + | ||||
|     " <node id=\"1682874165\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2160741\" lon=\"3.2191659\"/>\n" + | ||||
|     " <node id=\"1682874166\" visible=\"true\" version=\"2\" changeset=\"50433000\" timestamp=\"2017-07-20T13:11:33Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2163108\" lon=\"3.2188917\"/>\n" + | ||||
|     " <node id=\"1682875368\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2159045\" lon=\"3.2178572\"/>\n" + | ||||
|     " <node id=\"1682875370\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158354\" lon=\"3.2179572\"/>\n" + | ||||
|     " <node id=\"1682875372\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158939\" lon=\"3.2176815\"/>\n" + | ||||
|     " <node id=\"1682875373\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:24Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158483\" lon=\"3.2179326\"/>\n" + | ||||
|     " <node id=\"1682875374\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157153\" lon=\"3.2177946\"/>\n" + | ||||
|     " <node id=\"1682875381\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158748\" lon=\"3.2178159\"/>\n" + | ||||
|     " <node id=\"1682875385\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158683\" lon=\"3.2179618\"/>\n" + | ||||
|     " <node id=\"1682875387\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158782\" lon=\"3.2179443\"/>\n" + | ||||
|     " <node id=\"1682875389\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2158141\" lon=\"3.2177312\"/>\n" + | ||||
|     " <node id=\"1682875395\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157649\" lon=\"3.2177632\"/>\n" + | ||||
|     " <node id=\"1682876085\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155829\" lon=\"3.2178849\"/>\n" + | ||||
|     " <node id=\"1682876086\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156324\" lon=\"3.2178513\"/>\n" + | ||||
|     " <node id=\"1682876088\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156714\" lon=\"3.2180361\"/>\n" + | ||||
|     " <node id=\"1682876091\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156926\" lon=\"3.2178103\"/>\n" + | ||||
|     " <node id=\"1682876092\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156297\" lon=\"3.2178592\"/>\n" + | ||||
|     " <node id=\"1682876099\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157351\" lon=\"3.2180027\"/>\n" + | ||||
|     " <node id=\"1682876100\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2156190\" lon=\"3.2180623\"/>\n" + | ||||
|     " <node id=\"1682876102\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2157232\" lon=\"3.2179435\"/>\n" + | ||||
|     " <node id=\"1682877254\" visible=\"true\" version=\"3\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154254\" lon=\"3.2179953\"/>\n" + | ||||
|     " <node id=\"1682877255\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155716\" lon=\"3.2180819\"/>\n" + | ||||
|     " <node id=\"1682877256\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2155250\" lon=\"3.2179243\"/>\n" + | ||||
|     " <node id=\"1682877257\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154821\" lon=\"3.2181863\"/>\n" + | ||||
|     " <node id=\"1682879309\" visible=\"true\" version=\"2\" changeset=\"50435292\" timestamp=\"2017-07-20T15:06:25Z\" user=\"catweazle67\" uid=\"1976209\" lat=\"51.2154292\" lon=\"3.2182116\"/>\n" + | ||||
|     " </osm>" | ||||
| 
 | ||||
| const url = "https://www.openstreetmap.org/api/0.6/map?bbox=3.217620849609375,51.21548639922819,3.218994140625,51.21634661126673" | ||||
| Utils.downloadJson(url).then(data =>{ | ||||
|     const osmSource = { | ||||
|         rawDataHandlers : [] | ||||
|     } | ||||
|     new FullNodeDatabaseSource(osmSource) | ||||
|     osmSource.rawDataHandlers[0]( data, 0) | ||||
| import ShowDataLayer from "./UI/ShowDataLayer/ShowDataLayer"; | ||||
| import AllKnownLayers from "./Customizations/AllKnownLayers"; | ||||
| import Minimap from "./UI/Base/Minimap"; | ||||
| import StaticFeatureSource from "./Logic/FeatureSource/Sources/StaticFeatureSource"; | ||||
| import MinimapImplementation from "./UI/Base/MinimapImplementation"; | ||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; | ||||
| import BaseLayer from "./Models/BaseLayer"; | ||||
| import {UIEventSource} from "./Logic/UIEventSource"; | ||||
| import AvailableBaseLayersImplementation from "./Logic/Actors/AvailableBaseLayersImplementation"; | ||||
| MinimapImplementation.initialize() | ||||
| AvailableBaseLayers.implement(new AvailableBaseLayersImplementation()) | ||||
| const confirmationMap = Minimap.createMiniMap({ | ||||
|     background: new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) | ||||
| }) | ||||
| const features = [{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":1728823483},"geometry":{"type":"LineString","coordinates":[[3.216693,51.2147409],[3.2166930000000225,51.214740500000055]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":1728823481},"geometry":{"type":"LineString","coordinates":[[3.2167247,51.2146969],[3.21671060000004,51.2147159000002]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":1728823481},"geometry":{"type":"LineString","coordinates":[[3.2167247,51.2146969],[3.2167241999999976,51.214696799999714]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":1728823549},"geometry":{"type":"LineString","coordinates":[[3.2168871,51.2147399],[3.2168876999999547,51.21474009999989]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289383},"geometry":{"type":"LineString","coordinates":[[3.2169973,51.2147676],[3.2169969000000034,51.21476780000005]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289388},"geometry":{"type":"LineString","coordinates":[[3.2169829,51.2147884],[3.2169673999999895,51.21481170000002]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289388},"geometry":{"type":"LineString","coordinates":[[3.2169829,51.2147884],[3.216949899999979,51.214808000000225]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289388},"geometry":{"type":"LineString","coordinates":[[3.2169829,51.2147884],[3.2169306,51.21480400000028]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289388},"geometry":{"type":"LineString","coordinates":[[3.2169829,51.2147884],[3.2169465999999756,51.214779199999825]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978288381},"geometry":{"type":"LineString","coordinates":[[3.2168856,51.2147638],[3.216885599999961,51.214763799999986]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289386},"geometry":{"type":"LineString","coordinates":[[3.2168815,51.2147718],[3.216881100000038,51.21477160000009]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":4978289384},"geometry":{"type":"LineString","coordinates":[[3.2168674,51.2147683],[3.216867399999983,51.214768400000224]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":1728823514},"geometry":{"type":"LineString","coordinates":[[3.2168551,51.2147863],[3.2168551000000436,51.21478629999984]]}},"freshness":"2021-11-02T20:06:53.088Z"},{"feature":{"type":"Feature","properties":{"move":"yes","osm-id":1728823483},"geometry":{"type":"LineString","coordinates":[[3.216693,51.2147409],[3.2166930000000225,51.214740500000055]]}},"freshness":"2021-11-02T20:06:53.088Z"}] | ||||
| const changePreview = new StaticFeatureSource(features.map(f => f.feature), false) | ||||
| console.log("ChangePreview", changePreview.features.data) | ||||
| new ShowDataLayer({ | ||||
|     leafletMap: confirmationMap.leafletMap, | ||||
|     enablePopups: false, | ||||
|     zoomToFeatures: true, | ||||
|     features: changePreview, | ||||
|     layerToShow: AllKnownLayers.sharedLayers.get("conflation") | ||||
| }) | ||||
|              | ||||
| confirmationMap.SetStyle("height: 20rem").SetClass("w-full").AttachTo("maindiv") | ||||
							
								
								
									
										185
									
								
								test/ReplaceGeometry.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										185
									
								
								test/ReplaceGeometry.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,185 @@ | |||
| import T from "./TestHelper"; | ||||
| import FullNodeDatabaseSource from "../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; | ||||
| import {Utils} from "../Utils"; | ||||
| 
 | ||||
| export default class ReplaceGeometrySpec extends T { | ||||
|     constructor() { | ||||
|         super("ReplaceGeometry", [ | ||||
|             ["Simple house replacement", async () => { | ||||
|                 const coordinates = <[number, number][]>[[ | ||||
|                     3.216690793633461, | ||||
|                     51.21474084112525 | ||||
|                 ], | ||||
|                     [ | ||||
|                         3.2167256623506546, | ||||
|                         51.214696737309964 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2169999182224274, | ||||
|                         51.214768983537674 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2169650495052338, | ||||
|                         51.21480720678671 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2169368863105774, | ||||
|                         51.21480090625335 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2169489562511444, | ||||
|                         51.21478074454077 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.216886594891548, | ||||
|                         51.214765203214625 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2168812304735184, | ||||
|                         51.21477192378873 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2168644666671753, | ||||
|                         51.214768983537674 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.2168537378311157, | ||||
|                         51.21478746511261 | ||||
|                     ], | ||||
|                     [ | ||||
|                         3.216690793633461, | ||||
|                         51.21474084112525 | ||||
|                     ] | ||||
|                 ] | ||||
| 
 | ||||
|                 Utils.injectJsonDownloadForTests( | ||||
|                     "https://www.openstreetmap.org/api/0.6/way/160909312/full", | ||||
|                     { | ||||
|                         "version": "0.6", | ||||
|                         "generator": "CGImap 0.8.5 (920083 spike-06.openstreetmap.org)", | ||||
|                         "copyright": "OpenStreetMap and contributors", | ||||
|                         "attribution": "http://www.openstreetmap.org/copyright", | ||||
|                         "license": "http://opendatacommons.org/licenses/odbl/1-0/", | ||||
|                         "elements": [{ | ||||
|                             "type": "node", | ||||
|                             "id": 1728823481, | ||||
|                             "lat": 51.2146969, | ||||
|                             "lon": 3.2167247, | ||||
|                             "timestamp": "2017-07-18T22:52:45Z", | ||||
|                             "version": 2, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 1728823483, | ||||
|                             "lat": 51.2147409, | ||||
|                             "lon": 3.216693, | ||||
|                             "timestamp": "2017-07-18T22:52:45Z", | ||||
|                             "version": 2, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 1728823514, | ||||
|                             "lat": 51.2147863, | ||||
|                             "lon": 3.2168551, | ||||
|                             "timestamp": "2017-07-18T22:52:45Z", | ||||
|                             "version": 2, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 1728823549, | ||||
|                             "lat": 51.2147399, | ||||
|                             "lon": 3.2168871, | ||||
|                             "timestamp": "2017-07-18T22:52:46Z", | ||||
|                             "version": 2, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 4978288381, | ||||
|                             "lat": 51.2147638, | ||||
|                             "lon": 3.2168856, | ||||
|                             "timestamp": "2017-07-18T22:52:21Z", | ||||
|                             "version": 1, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 4978289383, | ||||
|                             "lat": 51.2147676, | ||||
|                             "lon": 3.2169973, | ||||
|                             "timestamp": "2017-07-18T22:52:21Z", | ||||
|                             "version": 1, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 4978289384, | ||||
|                             "lat": 51.2147683, | ||||
|                             "lon": 3.2168674, | ||||
|                             "timestamp": "2017-07-18T22:52:21Z", | ||||
|                             "version": 1, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 4978289386, | ||||
|                             "lat": 51.2147718, | ||||
|                             "lon": 3.2168815, | ||||
|                             "timestamp": "2017-07-18T22:52:21Z", | ||||
|                             "version": 1, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "node", | ||||
|                             "id": 4978289388, | ||||
|                             "lat": 51.2147884, | ||||
|                             "lon": 3.2169829, | ||||
|                             "timestamp": "2017-07-18T22:52:21Z", | ||||
|                             "version": 1, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209 | ||||
|                         }, { | ||||
|                             "type": "way", | ||||
|                             "id": 160909312, | ||||
|                             "timestamp": "2017-07-18T22:52:30Z", | ||||
|                             "version": 2, | ||||
|                             "changeset": 50391526, | ||||
|                             "user": "catweazle67", | ||||
|                             "uid": 1976209, | ||||
|                             "nodes": [1728823483, 1728823514, 4978289384, 4978289386, 4978288381, 4978289388, 4978289383, 1728823549, 1728823481, 1728823483], | ||||
|                             "tags": { | ||||
|                                 "addr:city": "Brugge", | ||||
|                                 "addr:country": "BE", | ||||
|                                 "addr:housenumber": "108", | ||||
|                                 "addr:postcode": "8000", | ||||
|                                 "addr:street": "Ezelstraat", | ||||
|                                 "building": "yes" | ||||
|                             } | ||||
|                         }] | ||||
|                     } | ||||
|                 ) | ||||
| 
 | ||||
| 
 | ||||
|                 const wayId = "way/160909312" | ||||
|                 const url = `https://www.openstreetmap.org/api/0.6/${wayId}/full`; | ||||
|                 const rawData = await Utils.downloadJsonCached(url, 1000) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             }] | ||||
|         ]); | ||||
|     } | ||||
| } | ||||
|  | @ -13,6 +13,7 @@ import TileFreshnessCalculatorSpec from "./TileFreshnessCalculator.spec"; | |||
| import WikidataSpecTest from "./Wikidata.spec.test"; | ||||
| import ImageProviderSpec from "./ImageProvider.spec"; | ||||
| import ActorsSpec from "./Actors.spec"; | ||||
| import ReplaceGeometrySpec from "./ReplaceGeometry.spec"; | ||||
| 
 | ||||
| 
 | ||||
| ScriptUtils.fixUtils() | ||||
|  | @ -29,7 +30,8 @@ const allTests = [ | |||
|     new TileFreshnessCalculatorSpec(), | ||||
|     new WikidataSpecTest(), | ||||
|     new ImageProviderSpec(), | ||||
|     new ActorsSpec() | ||||
|     new ActorsSpec(), | ||||
|     new ReplaceGeometrySpec() | ||||
| ] | ||||
| 
 | ||||
| Utils.externalDownloadFunction = async (url) => { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue