diff --git a/Logic/Actors/GeoLocationHandler.ts b/Logic/Actors/GeoLocationHandler.ts index 6e3c7bd75..c508f8902 100644 --- a/Logic/Actors/GeoLocationHandler.ts +++ b/Logic/Actors/GeoLocationHandler.ts @@ -58,10 +58,15 @@ export default class GeoLocationHandler extends VariableUiElement { private readonly _layoutToUse: LayoutConfig; constructor( - currentGPSLocation: UIEventSource, - leafletMap: UIEventSource, - layoutToUse: LayoutConfig + state: { + currentGPSLocation: UIEventSource, + leafletMap: UIEventSource, + layoutToUse: LayoutConfig, + featureSwitchGeolocation: UIEventSource + } ) { + const currentGPSLocation = state.currentGPSLocation + const leafletMap = state.leafletMap const hasLocation = currentGPSLocation.map( (location) => location !== undefined ); @@ -122,7 +127,7 @@ export default class GeoLocationHandler extends VariableUiElement { this._previousLocationGrant = previousLocationGrant; this._currentGPSLocation = currentGPSLocation; this._leafletMap = leafletMap; - this._layoutToUse = layoutToUse; + this._layoutToUse = state.layoutToUse; this._hasLocation = hasLocation; const self = this; @@ -167,7 +172,7 @@ export default class GeoLocationHandler extends VariableUiElement { const latLonGiven = QueryParameters.wasInitialized("lat") && QueryParameters.wasInitialized("lon") - this.init(false, !latLonGiven); + this.init(false, !latLonGiven && state.featureSwitchGeolocation.data); isLocked.addCallbackAndRunD(isLocked => { if (isLocked) { @@ -208,7 +213,7 @@ export default class GeoLocationHandler extends VariableUiElement { } - private init(askPermission: boolean, forceZoom: boolean) { + private init(askPermission: boolean, zoomToLocation: boolean) { const self = this; if (self._isActive.data) { @@ -222,7 +227,7 @@ export default class GeoLocationHandler extends VariableUiElement { ?.then(function (status) { console.log("Geolocation permission is ", status.state); if (status.state === "granted") { - self.StartGeolocating(forceZoom); + self.StartGeolocating(zoomToLocation); } self._permission.setData(status.state); status.onchange = function () { @@ -234,10 +239,10 @@ export default class GeoLocationHandler extends VariableUiElement { } if (askPermission) { - self.StartGeolocating(forceZoom); + self.StartGeolocating(zoomToLocation); } else if (this._previousLocationGrant.data === "granted") { this._previousLocationGrant.setData(""); - self.StartGeolocating(forceZoom); + self.StartGeolocating(zoomToLocation); } } diff --git a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts index 026e164f7..750f6d037 100644 --- a/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts +++ b/Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource.ts @@ -3,21 +3,6 @@ import FeatureSource, {FeatureSourceForLayer, Tiled} from "../FeatureSource"; import {OsmNode, OsmObject, OsmWay} from "../../Osm/OsmObject"; import SimpleFeatureSource from "../Sources/SimpleFeatureSource"; 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 { @@ -34,70 +19,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy, - 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) @@ -143,8 +65,3 @@ export default class FullNodeDatabaseSource implements TileHierarchy { + + const features = [] + let geometryMoved = false; + for (let i = 0; i < this._coordinateInfo.length; i++) { + const coordinateInfo = this._coordinateInfo[i]; + if (coordinateInfo.identicalTo !== undefined) { + continue + } + if (coordinateInfo.closebyNodes === undefined || coordinateInfo.closebyNodes.length === 0) { + + const newPoint = { + type: "Feature", + properties: { + "newpoint": "yes", + id: "new-geometry-with-reuse-" + i + }, + geometry: { + type: "Point", + coordinates: coordinateInfo.lngLat + } + }; + features.push(newPoint) + continue + } + + const reusedPoint = coordinateInfo.closebyNodes[0] + if (reusedPoint.config.mode === "move_osm_point") { + const moveDescription = { + type: "Feature", + properties: { + "move": "yes", + "osm-id": reusedPoint.node.properties.id, + "id": "new-geometry-move-existing" + i + }, + geometry: { + type: "LineString", + coordinates: [reusedPoint.node.geometry.coordinates, coordinateInfo.lngLat] + } + } + features.push(moveDescription) + + } else { + // The geometry is moved + geometryMoved = true + } + } + + if (geometryMoved) { + + const coords: [number, number][] = [] + for (const info of this._coordinateInfo) { + if (info.identicalTo !== undefined) { + coords.push(coords[info.identicalTo]) + continue + } + + if (info.closebyNodes === undefined || info.closebyNodes.length === 0) { + coords.push(coords[info.identicalTo]) + continue + } + + const closest = info.closebyNodes[0] + if (closest.config.mode === "reuse_osm_point") { + coords.push(closest.node.geometry.coordinates) + } else { + coords.push(info.lngLat) + } + + } + const newGeometry = { + type: "Feature", + properties: { + "resulting-geometry": "yes", + "id": "new-geometry" + }, + geometry: { + type: "LineString", + coordinates: coords + } + } + features.push(newGeometry) + + } + console.log("Preview:", features) + return new StaticFeatureSource(features, false) + } + + protected async CreateChangeDescriptions(changes: Changes): Promise { + const theme = this._state.layoutToUse.id + const allChanges: ChangeDescription[] = [] + const nodeIdsToUse: { lat: number, lon: number, nodeId?: number }[] = [] + for (let i = 0; i < this._coordinateInfo.length; i++) { + const info = this._coordinateInfo[i] + const lat = info.lngLat[1] + const lon = info.lngLat[0] + + if (info.identicalTo !== undefined) { + nodeIdsToUse.push(nodeIdsToUse[info.identicalTo]) + continue + } + if (info.closebyNodes === undefined || info.closebyNodes[0] === undefined) { + const newNodeAction = new CreateNewNodeAction([], lat, lon, { + allowReuseOfPreviouslyCreatedPoints: true, + changeType: null, + theme + }) + + allChanges.push(...(await newNodeAction.CreateChangeDescriptions(changes))) + + nodeIdsToUse.push({ + lat, lon, + nodeId : newNodeAction.newElementIdNumber}) + continue + + } + + const closestPoint = info.closebyNodes[0] + const id = Number(closestPoint.node.properties.id.split("/")[1]) + if(closestPoint.config.mode === "move_osm_point"){ + allChanges.push({ + type: "node", + id, + changes: { + lat, lon + }, + meta: { + theme, + changeType: null + } + }) + } + nodeIdsToUse.push({lat, lon, nodeId: id}) + } + + + const newWay = new CreateNewWayAction(this._tags, nodeIdsToUse, { + theme + }) + + allChanges.push(...(await newWay.Perform(changes))) + + return allChanges + } + + private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] { + + const bbox = new BBox(coordinates) + const state = this._state + const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2))) + const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM)) + + const coordinateInfo: { + lngLat: [number, number], + identicalTo?: number, + closebyNodes?: { + d: number, + node: any, + config: MergePointConfig + }[] + }[] = coordinates.map(_ => undefined) + + for (let i = 0; i < coordinates.length; i++) { + + if (coordinateInfo[i] !== undefined) { + // Already seen, probably a duplicate coordinate + continue + } + const coor = coordinates[i] + // Check closeby (and probably identical) point further in the coordinate list, mark them as duplicate + for (let j = i + 1; j < coordinates.length; j++) { + if (1000 * GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) { + coordinateInfo[j] = { + lngLat: coor, + identicalTo: i + } + break; + } + } + + // Gather the actual info for this point + + // Lets search applicable points and determine the merge mode + const closebyNodes: { + d: number, + node: any, + config: MergePointConfig + }[] = [] + for (const node of allNodes) { + const center = node.geometry.coordinates + const d = 1000 * GeoOperations.distanceBetween(coor, center) + if (d > maxDistance) { + continue + } + + for (const config of this._config) { + if (d > config.withinRangeOfM) { + continue + } + if (!config.ifMatches.matchesProperties(node.properties)) { + continue + } + closebyNodes.push({node, d, config}) + } + } + + closebyNodes.sort((n0, n1) => { + return n0.d - n1.d + }) + + coordinateInfo[i] = { + identicalTo: undefined, + lngLat: coor, + closebyNodes + } + + } + + let conflictFree = true; + + do { + conflictFree = true; + for (let i = 0; i < coordinateInfo.length; i++) { + + const coorInfo = coordinateInfo[i] + if (coorInfo.identicalTo !== undefined) { + continue + } + if (coorInfo.closebyNodes === undefined || coorInfo.closebyNodes[0] === undefined) { + continue + } + + for (let j = i + 1; j < coordinates.length; j++) { + const other = coordinateInfo[j] + if (other.closebyNodes === undefined || other.closebyNodes[0] === undefined) { + continue + } + + if (other.closebyNodes[0].node === coorInfo.closebyNodes[0].node) { + conflictFree = false + // We have found a conflict! + // We only keep the closest point + if (other.closebyNodes[0].d > coorInfo.closebyNodes[0].d) { + other.closebyNodes.shift() + } else { + coorInfo.closebyNodes.shift() + } + } + } + } + } while (!conflictFree) + + + return coordinateInfo + } + +} \ No newline at end of file diff --git a/Logic/Osm/Actions/ReplaceGeometryAction.ts b/Logic/Osm/Actions/ReplaceGeometryAction.ts index 72195ff7f..cc56ff03e 100644 --- a/Logic/Osm/Actions/ReplaceGeometryAction.ts +++ b/Logic/Osm/Actions/ReplaceGeometryAction.ts @@ -19,7 +19,15 @@ export default class ReplaceGeometryAction extends OsmChangeAction { }; private readonly wayToReplaceId: string; private readonly theme: string; + /** + * The target coordinates that should end up in OpenStreetMap + */ private readonly targetCoordinates: [number, number][]; + /** + * If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index. + * @private + */ + private readonly identicalTo: number[] private readonly newTags: Tag[] | undefined; constructor( @@ -46,13 +54,36 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } else if (geom.type === "Polygon") { coordinates = geom.coordinates[0] } + + this.identicalTo = coordinates.map(_ => undefined) + + for (let i = 0; i < coordinates.length; i++) { + if (this.identicalTo[i] !== undefined) { + continue + } + for (let j = i + 1; j < coordinates.length; j++) { + const d = 1000 * GeoOperations.distanceBetween(coordinates[i], coordinates[j]) + if (d < 0.1) { + console.log("Identical coordinates detected: ", i, " and ", j, ": ", coordinates[i], coordinates[j], "distance is", d) + this.identicalTo[j] = i + } + } + } + + this.targetCoordinates = coordinates this.newTags = options.newTags } - public async GetPreview(): Promise { + public async getPreview(): Promise { const {closestIds, allNodesById} = await this.GetClosestIds(); + console.log("Generating preview, identicals are ", ) const preview = closestIds.map((newId, i) => { + if(this.identicalTo[i] !== undefined){ + return undefined + } + + if (newId === undefined) { return { type: "Feature", @@ -80,7 +111,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } }; }) - return new StaticFeatureSource(preview, false) + return new StaticFeatureSource(Utils.NoNull(preview), false) } @@ -92,6 +123,11 @@ export default class ReplaceGeometryAction extends OsmChangeAction { const {closestIds, osmWay} = await this.GetClosestIds() for (let i = 0; i < closestIds.length; i++) { + if(this.identicalTo[i] !== undefined){ + const j = this.identicalTo[i] + actualIdsToUse.push(actualIdsToUse[j]) + continue + } const closestId = closestIds[i]; const [lon, lat] = this.targetCoordinates[i] if (closestId === undefined) { @@ -161,7 +197,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction { 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 + // TODO FIXME: detect intersections with other ways if moved const splitted = this.wayToReplaceId.split("/"); const type = splitted[0]; const idN = Number(splitted[1]); @@ -185,7 +221,8 @@ export default class ReplaceGeometryAction extends OsmChangeAction { const closestIds = [] const distances = [] - for (const target of this.targetCoordinates) { + for (let i = 0; i < this.targetCoordinates.length; i++){ + const target = this.targetCoordinates[i]; let closestDistance = undefined let closestId = undefined; for (const osmNode of allNodes) { @@ -202,9 +239,18 @@ export default class ReplaceGeometryAction extends OsmChangeAction { } // Next step: every closestId can only occur once in the list + // We skip the ones which are identical + console.log("Erasing double ids") for (let i = 0; i < closestIds.length; i++) { + if(this.identicalTo[i] !== undefined){ + closestIds[i] = closestIds[this.identicalTo[i]] + continue + } const closestId = closestIds[i] for (let j = i + 1; j < closestIds.length; j++) { + if(this.identicalTo[j] !== undefined){ + continue + } const otherClosestId = closestIds[j] if (closestId !== otherClosestId) { continue diff --git a/Models/Constants.ts b/Models/Constants.ts index 8d7dab35e..03e6aede0 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import {Utils} from "../Utils"; export default class Constants { - public static vNumber = "0.12.1-beta"; + public static vNumber = "0.12.2-beta"; public static ImgurApiKey = '7070e7167f0a25a' public static readonly mapillary_client_token_v3 = 'TXhLaWthQ1d4RUg0czVxaTVoRjFJZzowNDczNjUzNmIyNTQyYzI2' public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" diff --git a/UI/BigComponents/ImportButton.ts b/UI/BigComponents/ImportButton.ts index fd35a72db..5175132d9 100644 --- a/UI/BigComponents/ImportButton.ts +++ b/UI/BigComponents/ImportButton.ts @@ -32,6 +32,10 @@ import StaticFeatureSource from "../../Logic/FeatureSource/Sources/StaticFeature import ShowDataMultiLayer from "../ShowDataLayer/ShowDataMultiLayer"; import BaseLayer from "../../Models/BaseLayer"; import ReplaceGeometryAction from "../../Logic/Osm/Actions/ReplaceGeometryAction"; +import FullNodeDatabaseSource from "../../Logic/FeatureSource/TiledFeatureSource/FullNodeDatabaseSource"; +import CreateWayWithPointReuseAction from "../../Logic/Osm/Actions/CreateWayWithPointReuseAction"; +import OsmChangeAction from "../../Logic/Osm/Actions/OsmChangeAction"; +import FeatureSource from "../../Logic/FeatureSource/FeatureSource"; export interface ImportButtonState { @@ -282,7 +286,7 @@ export default class ImportButton extends Toggle { importClicked: UIEventSource): BaseUIElement { const confirmationMap = Minimap.createMiniMap({ - allowMoving: false, + allowMoving: true, background: o.state.backgroundLayer }) confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl") @@ -297,15 +301,15 @@ export default class ImportButton extends Toggle { allElements: o.state.allElements, layers: o.state.filteredLayers }) + + let action : OsmChangeAction & {getPreview() : Promise} const theme = o.state.layoutToUse.id - - const changes = o.state.changes let confirm: () => Promise if (o.conflationSettings !== undefined) { - let replaceGeometryAction = new ReplaceGeometryAction( + action = new ReplaceGeometryAction( o.state, o.feature, o.conflationSettings.conflateWayId, @@ -314,41 +318,54 @@ export default class ImportButton extends Toggle { 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) + changes.applyAction (action) return o.feature.properties.id } } else { + 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] + } + + + action = new CreateWayWithPointReuseAction( + o.newTags.data, + coordinates, + // @ts-ignore + o.state, + [{ + withinRangeOfM: 1, + ifMatches: new Tag("_is_part_of_building","true"), + mode:"move_osm_point" + + }] + ) + + 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 + changes.applyAction(action) + return undefined } } + action.getPreview().then(changePreview => { + new ShowDataLayer({ + leafletMap: confirmationMap.leafletMap, + enablePopups: false, + zoomToFeatures: false, + features: changePreview, + allElements: o.state.allElements, + layerToShow: AllKnownLayers.sharedLayers.get("conflation") + }) + }) + const confirmButton = new SubtleButton(o.image(), o.message) confirmButton.onClick(async () => { { diff --git a/UI/BigComponents/RightControls.ts b/UI/BigComponents/RightControls.ts index 352fc11e9..35f11754c 100644 --- a/UI/BigComponents/RightControls.ts +++ b/UI/BigComponents/RightControls.ts @@ -12,9 +12,7 @@ export default class RightControls extends Combine { constructor(state:MapState) { const geolocatioHandler = new GeoLocationHandler( - state.currentGPSLocation, - state.leafletMap, - state.layoutToUse + state ) new ShowDataLayer({ diff --git a/assets/layers/conflation/conflation.json b/assets/layers/conflation/conflation.json index 2f638cfa5..66ea6655a 100644 --- a/assets/layers/conflation/conflation.json +++ b/assets/layers/conflation/conflation.json @@ -30,7 +30,17 @@ }, { "width": "3", - "color": "#00f" + "color": "#00f", + + "dasharray": { + "render": "", + "mappings": [ + { + "if": "resulting-geometry=yes", + "then": "6 6" + } + ] + } } ] } \ No newline at end of file diff --git a/assets/themes/grb_import/grb.json b/assets/themes/grb_import/grb.json index 5a3d5d545..a2fcbcf32 100644 --- a/assets/themes/grb_import/grb.json +++ b/assets/themes/grb_import/grb.json @@ -29,6 +29,7 @@ "minzoom": 18 }, "trackAllNodes": true, + "enableGeolocation": false, "layers": [ { "builtin": "type_node", @@ -41,6 +42,13 @@ "_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" + ], + "mapRendering": [ + { + "icon": "square:#00f", + "iconSize": "5,5,center", + "location": "point" + } ] } }, @@ -638,7 +646,8 @@ "_grb_ref=feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']", "_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties._grb_ref", "_grb_date=feat.properties['source:geometry:date'].replace(/\\//g,'-')", - "_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date" + "_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date", + "_target_building_type=feat.properties['_osm_obj:building'] === 'yes' ? feat.properties.building : (feat.properties['_osm_obj:building'] ?? feat.properties.building)" ], "tagRenderings": [ { @@ -676,7 +685,7 @@ "mappings": [ { "if": "_overlaps_with!=null", - "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)}" + "then": "{import_button(OSM-buildings,building=$_target_building_type; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Replace the geometry in OpenStreetMap,,,_osm_obj:id)}" } ] },