From 2484848cd6c364b328fc8a7eda5146cfd6e55024 Mon Sep 17 00:00:00 2001 From: pietervdvn Date: Wed, 3 Nov 2021 00:44:53 +0100 Subject: [PATCH] Refactoring of GPS-location (uses featureSource too now), factoring out state, add ReplaceGeometryAction and conflation example --- Logic/Actors/GeoLocationHandler.ts | 86 +++--- Logic/BBox.ts | 5 + Logic/FeatureSource/FeaturePipeline.ts | 6 +- .../NewGeometryFromChangesFeatureSource.ts | 2 +- .../RenderingMultiPlexerFeatureSource.ts | 4 +- .../FullNodeDatabaseSource.ts | 108 ++++++- .../TiledFeatureSource/OsmFeatureSource.ts | 6 +- Logic/Osm/Actions/ChangeTagAction.ts | 11 +- Logic/Osm/Actions/CreateNewWayAction.ts | 20 +- Logic/Osm/Actions/ReplaceGeometryAction.ts | 232 +++++++++++++++ Logic/Osm/Changes.ts | 12 +- Logic/State/MapState.ts | 9 +- UI/Base/AsyncLazy.ts | 28 ++ UI/Base/Minimap.ts | 1 + UI/Base/MinimapImplementation.ts | 7 + UI/BigComponents/FullWelcomePaneWithTabs.ts | 14 +- UI/BigComponents/ImportButton.ts | 264 +++++++++++++----- UI/BigComponents/LeftControls.ts | 6 +- UI/BigComponents/RightControls.ts | 23 +- UI/BigComponents/ShareScreen.ts | 7 +- UI/DefaultGUI.ts | 75 +---- UI/DefaultGuiState.ts | 74 +++++ UI/ExportPDF.ts | 25 +- UI/Input/LocationInput.ts | 3 + UI/Popup/FeatureInfoBox.ts | 1 - UI/ShowDataLayer/ShowDataLayer.ts | 6 +- UI/SpecialVisualizations.ts | 4 +- UI/SubstitutedTranslation.ts | 2 +- assets/layers/conflation/conflation.json | 32 +++ assets/layers/gps_location/gps_location.json | 15 + .../layers/home_location/home_location.json | 46 +-- .../themes/cycle_highways/cycle_highways.json | 5 - assets/themes/grb_import/grb.json | 6 +- index.ts | 3 +- test.ts | 165 ++--------- test/ReplaceGeometry.spec.ts | 185 ++++++++++++ test/TestAll.ts | 4 +- 37 files changed, 1035 insertions(+), 467 deletions(-) create mode 100644 Logic/Osm/Actions/ReplaceGeometryAction.ts create mode 100644 UI/Base/AsyncLazy.ts create mode 100644 UI/DefaultGuiState.ts create mode 100644 assets/layers/conflation/conflation.json create mode 100644 assets/layers/gps_location/gps_location.json create mode 100644 test/ReplaceGeometry.spec.ts diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 86e656b505..5927b73cbd 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -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; - /*** - * The marker on the map, in order to update it - * @private - */ - private _marker: L.Marker; /** * Literally: _currentGPSLocation.data != undefined * @private */ private readonly _hasLocation: UIEventSource; - private readonly _currentGPSLocation: UIEventSource<{ - latlng: any; - accuracy: number; - }>; + private readonly _currentGPSLocation: UIEventSource; /** * 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, + currentGPSLocation: UIEventSource, + leafletMap: UIEventSource, 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"); diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 78634897b6..ccd3201257 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -116,6 +116,11 @@ export class BBox { getSouth() { return this.minLat } + + contains(lonLat: [number, number]){ + return this.minLat <= lonLat[1] && lonLat[1] <= this.maxLat + && this.minLon<= lonLat[0] && lonLat[0] <= this.maxLon + } pad(factor: number, maxIncrease = 2): BBox { diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index ecec7cfded..4e90f1616a 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -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)) } diff --git a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts index 92138b9951..bd73f04c26 100644 --- a/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts +++ b/Logic/FeatureSource/Sources/NewGeometryFromChangesFeatureSource.ts @@ -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": diff --git a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts index 5d2161a4e9..eb0d4b10df 100644 --- a/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts +++ b/Logic/FeatureSource/Sources/RenderingMultiPlexerFeatureSource.ts @@ -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) { diff --git a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts index 20d7bee07b..026e164f75 100644 --- a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts @@ -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 { public readonly loadedTiles = new Map() private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void; - private readonly layer : FilteredLayer - + private readonly layer: FilteredLayer + constructor( - state: { - readonly filteredLayers: UIEventSource}, - 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, + 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() @@ -57,7 +130,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy ({ - feature: osmNode.asGeoJson(),freshness: now + feature: osmNode.asGeoJson(), freshness: now })) const featureSource = new SimpleFeatureSource(this.layer, tileId) @@ -66,5 +139,12 @@ export default class FullNodeDatabaseSource implements TileHierarchy { - 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, diff --git a/Logic/Osm/Actions/ChangeTagAction.ts b/Logic/Osm/Actions/ChangeTagAction.ts index 00e9b001ed..862d36629f 100644 --- a/Logic/Osm/Actions/ChangeTagAction.ts +++ b/Logic/Osm/Actions/ChangeTagAction.ts @@ -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; } diff --git a/Logic/Osm/Actions/CreateNewWayAction.ts b/Logic/Osm/Actions/CreateNewWayAction.ts index ec54861210..48b7ec7fbe 100644 --- a/Logic/Osm/Actions/CreateNewWayAction.ts +++ b/Logic/Osm/Actions/CreateNewWayAction.ts @@ -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; diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts new file mode 100644 index 0000000000..72195ff7f6 --- /dev/null +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -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 { + 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 { + + 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 = { + 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, 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() + const allNodes = parsed.filter(o => o.type === "node") + for (const node of allNodes) { + allNodesById.set(node.id, 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 = parsed[parsed.length - 1] + if (osmWay.type !== "way") { + throw "WEIRD: expected an OSM-way as last element here!" + } + return {closestIds, allNodesById, osmWay}; + } + + +} \ No newline at end of file diff --git a/Logic/Osm/Changes.ts b/Logic/Osm/Changes.ts index 48fd10cf7a..72e5434c5a 100644 --- a/Logic/Osm/Changes.ts +++ b/Logic/Osm/Changes.ts @@ -114,7 +114,16 @@ export class Changes { } public async applyAction(action: OsmChangeAction): Promise { - 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 diff --git a/Logic/State/MapState.ts b/Logic/State/MapState.ts index 00a05dd61c..8939149f50 100644 --- a/Logic/State/MapState.ts +++ b/Logic/State/MapState.ts @@ -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 = new UIEventSource(undefined); public readonly mainMapObject: BaseUIElement & MinimapObj; diff --git a/UI/Base/AsyncLazy.ts b/UI/Base/AsyncLazy.ts new file mode 100644 index 0000000000..b8db53d3c8 --- /dev/null +++ b/UI/Base/AsyncLazy.ts @@ -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; + + constructor(f: () => Promise) { + 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() + } + +} \ No newline at end of file diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts index 32cdadbd63..4a878684e0 100644 --- a/UI/Base/Minimap.ts +++ b/UI/Base/Minimap.ts @@ -19,6 +19,7 @@ export interface MinimapOptions { export interface MinimapObj { readonly leafletMap: UIEventSource, installBounds(factor: number | BBox, showRange?: boolean) : void + TakeScreenshot(): Promise; } export default class Minimap { diff --git a/UI/Base/MinimapImplementation.ts b/UI/Base/MinimapImplementation.ts index 249d4df6da..271b5d5cf5 100644 --- a/UI/Base/MinimapImplementation.ts +++ b/UI/Base/MinimapImplementation.ts @@ -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') + } } \ No newline at end of file diff --git a/UI/BigComponents/FullWelcomePaneWithTabs.ts b/UI/BigComponents/FullWelcomePaneWithTabs.ts index 59ca6e0c9e..da16326e47 100644 --- a/UI/BigComponents/FullWelcomePaneWithTabs.ts +++ b/UI/BigComponents/FullWelcomePaneWithTabs.ts @@ -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, - featureSwitchMoreQuests: UIEventSource + featureSwitchMoreQuests: UIEventSource, + locationControl: UIEventSource, + backgroundLayer: UIEventSource, + filteredLayers: UIEventSource } & UserRelatedState) { const layoutToUse = state.layoutToUse; super( @@ -39,7 +45,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { layoutToUse: LayoutConfig, osmConnection: OsmConnection, featureSwitchShareScreen: UIEventSource, - featureSwitchMoreQuests: UIEventSource + featureSwitchMoreQuests: UIEventSource, + locationControl: UIEventSource, backgroundLayer: UIEventSource, filteredLayers: UIEventSource } & UserRelatedState, isShown: UIEventSource): { header: string | BaseUIElement; content: BaseUIElement }[] { @@ -77,7 +84,8 @@ export default class FullWelcomePaneWithTabs extends ScrollableFullScreen { layoutToUse: LayoutConfig, osmConnection: OsmConnection, featureSwitchShareScreen: UIEventSource, - featureSwitchMoreQuests: UIEventSource + featureSwitchMoreQuests: UIEventSource, + locationControl: UIEventSource, backgroundLayer: UIEventSource, filteredLayers: UIEventSource } & UserRelatedState, currentTab: UIEventSource, isShown: UIEventSource) { const tabs = FullWelcomePaneWithTabs.ConstructBaseTabs(state, isShown) diff --git a/UI/BigComponents/ImportButton.ts b/UI/BigComponents/ImportButton.ts index 9c206f249f..fd35a72db2 100644 --- a/UI/BigComponents/ImportButton.ts +++ b/UI/BigComponents/ImportButton.ts @@ -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; + filteredLayers: UIEventSource; featureSwitchUserbadge: UIEventSource; featurePipeline: FeaturePipeline; allElements: ElementStorage; @@ -48,8 +56,14 @@ export interface ImportButtonState { locationControl: UIEventSource<{ zoom: number }> }, guiState: { filterViewIsOpened: UIEventSource }, - 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) - - 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") + let snapSettings = undefined + let conflationSettings = undefined + const possibleWayId = tagSource.data[args[5]] + if (possibleWayId?.startsWith("way/")) { + // This is a conflation + conflationSettings = { + conflateWayId: possibleWayId + } + } else { + + + 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, + importClicked: UIEventSource) { + 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, + importClicked: UIEventSource): 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 + 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, importClicked: UIEventSource): 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 = { - 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 = { + 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; - - } } \ No newline at end of file diff --git a/UI/BigComponents/LeftControls.ts b/UI/BigComponents/LeftControls.ts index f2459d0028..aff6b01f42 100644 --- a/UI/BigComponents/LeftControls.ts +++ b/UI/BigComponents/LeftControls.ts @@ -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, featureSwitchExportAsPdf: UIEventSource, filteredLayers: UIEventSource, - featureSwitchFilter: UIEventSource + featureSwitchFilter: UIEventSource, + backgroundLayer: UIEventSource, + osmConnection: OsmConnection }, guiState: { downloadControlIsOpened: UIEventSource, diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index a8853a54fa..352fc11e95 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -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 } ), diff --git a/UI/BigComponents/ShareScreen.ts b/UI/BigComponents/ShareScreen.ts index b112b15b05..9fc7dde910 100644 --- a/UI/BigComponents/ShareScreen.ts +++ b/UI/BigComponents/ShareScreen.ts @@ -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, backgroundLayer: UIEventSource, filteredLayers: UIEventSource}) { const layout = state?.layoutToUse; const tr = Translations.t.general.sharescreen; diff --git a/UI/DefaultGUI.ts b/UI/DefaultGUI.ts index 8ee02fdd4b..0c93e4bf87 100644 --- a/UI/DefaultGUI.ts +++ b/UI/DefaultGUI.ts @@ -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; - public readonly downloadControlIsOpened: UIEventSource; - public readonly filterViewIsOpened: UIEventSource; - public readonly copyrightViewIsOpened: UIEventSource; - public readonly welcomeMessageOpenedTab: UIEventSource - public readonly allFullScreenStates: UIEventSource[] = [] - 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"; /** diff --git a/UI/DefaultGuiState.ts b/UI/DefaultGuiState.ts new file mode 100644 index 0000000000..7c5cae1394 --- /dev/null +++ b/UI/DefaultGuiState.ts @@ -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; + public readonly downloadControlIsOpened: UIEventSource; + public readonly filterViewIsOpened: UIEventSource; + public readonly copyrightViewIsOpened: UIEventSource; + public readonly welcomeMessageOpenedTab: UIEventSource + public readonly allFullScreenStates: UIEventSource[] = [] + 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) + } + }) + } + } + + } +} \ No newline at end of file diff --git a/UI/ExportPDF.ts b/UI/ExportPDF.ts index e030e30b49..e031033221 100644 --- a/UI/ExportPDF.ts +++ b/UI/ExportPDF.ts @@ -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), // 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); diff --git a/UI/Input/LocationInput.ts b/UI/Input/LocationInput.ts index 65ecd604c8..21cfdebe73 100644 --- a/UI/Input/LocationInput.ts +++ b/UI/Input/LocationInput.ts @@ -167,6 +167,9 @@ export default class LocationInput extends InputElement implements MinimapO installBounds(factor: number | BBox, showRange?: boolean): void { this.map.installBounds(factor, showRange) } + TakeScreenshot(): Promise { + return this.map.TakeScreenshot() + } protected InnerConstructElement(): HTMLElement { try { diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index 9b95512a99..3ec2878565 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -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) } } diff --git a/UI/ShowDataLayer/ShowDataLayer.ts b/UI/ShowDataLayer/ShowDataLayer.ts index 309903348c..497c9282bc 100644 --- a/UI/ShowDataLayer/ShowDataLayer.ts +++ b/UI/ShowDataLayer/ShowDataLayer.ts @@ -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) } } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index fb9e4b6bc0..674b444993 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -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, diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index cf6e4af766..076073f759 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -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 { diff --git a/assets/layers/conflation/conflation.json b/assets/layers/conflation/conflation.json new file mode 100644 index 0000000000..2b80d1a36d --- /dev/null +++ b/assets/layers/conflation/conflation.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/assets/layers/gps_location/gps_location.json b/assets/layers/gps_location/gps_location.json new file mode 100644 index 0000000000..59c2c4ef06 --- /dev/null +++ b/assets/layers/gps_location/gps_location.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/assets/layers/home_location/home_location.json b/assets/layers/home_location/home_location.json index 0fb1af576c..c2073d3b88 100644 --- a/assets/layers/home_location/home_location.json +++ b/assets/layers/home_location/home_location.json @@ -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" + } + ] } \ No newline at end of file diff --git a/assets/themes/cycle_highways/cycle_highways.json b/assets/themes/cycle_highways/cycle_highways.json index 884ddf2ad5..0954bc8381 100644 --- a/assets/themes/cycle_highways/cycle_highways.json +++ b/assets/themes/cycle_highways/cycle_highways.json @@ -235,11 +235,6 @@ } ], "mapRendering": [ - { - "location": [ - "point" - ] - }, { "color": { "render": "#ff7392", diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index a7ce909e66..4ffa3b8d9b 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -38,7 +38,9 @@ "override": { "calculatedTags": [ "_is_part_of_building=feat.get('parent_ways')?.some(p => p.building !== undefined && p.building !== '') ?? false", - "_is_part_of_landuse=feat.get('parent_ways')?.some(p => (p.landuse !== undefined && p.landuse !== '') || (p.natural !== undefined && p.natural !== '')) ?? 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)}" } ] }, diff --git a/index.ts b/index.ts index 2ed85051ed..dcf030c48c 100644 --- a/index.ts +++ b/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() diff --git a/test.ts b/test.ts index 653048710a..7bc8c81d80 100644 --- a/test.ts +++ b/test.ts @@ -1,139 +1,26 @@ -import {Utils} from "./Utils"; -import FullNodeDatabaseSource from "./Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; - - -const data = "\n" + - "\n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " \n" + - " " - -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) -}) \ No newline at end of file +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(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") \ No newline at end of file diff --git a/test/ReplaceGeometry.spec.ts b/test/ReplaceGeometry.spec.ts new file mode 100644 index 0000000000..40c3e53bbb --- /dev/null +++ b/test/ReplaceGeometry.spec.ts @@ -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) + + + + + }] + ]); + } +} \ No newline at end of file diff --git a/test/TestAll.ts b/test/TestAll.ts index 9b23957d10..c6f64d62e0 100644 --- a/test/TestAll.ts +++ b/test/TestAll.ts @@ -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) => {