forked from MapComplete/MapComplete
Add unused node removal
This commit is contained in:
parent
4131e9b9e2
commit
adade2e8b0
12 changed files with 289 additions and 96 deletions
|
@ -85,7 +85,6 @@ class IntersectionFunc implements ExtraFunction {
|
||||||
const bbox = BBox.get(feat)
|
const bbox = BBox.get(feat)
|
||||||
|
|
||||||
for (const layerId of layerIds) {
|
for (const layerId of layerIds) {
|
||||||
console.log("Calculating the intersection with layer ", layerId)
|
|
||||||
const otherLayers = params.getFeaturesWithin(layerId, bbox)
|
const otherLayers = params.getFeaturesWithin(layerId, bbox)
|
||||||
if (otherLayers === undefined) {
|
if (otherLayers === undefined) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
@ -60,6 +60,12 @@ export default class FeaturePipeline {
|
||||||
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
|
private readonly localStorageSavers = new Map<string, SaveTileToLocalStorageActor>()
|
||||||
private readonly metataggingRecalculated = new UIEventSource<void>(undefined)
|
private readonly metataggingRecalculated = new UIEventSource<void>(undefined)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps track of all raw OSM-nodes.
|
||||||
|
* Only initialized if 'type_node' is defined as layer
|
||||||
|
*/
|
||||||
|
public readonly fullNodeDatabase? : FullNodeDatabaseSource
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
|
handleFeatureSource: (source: FeatureSourceForLayer & Tiled) => void,
|
||||||
state: MapState) {
|
state: MapState) {
|
||||||
|
@ -129,7 +135,14 @@ export default class FeaturePipeline {
|
||||||
this.freshnesses.set(id, new TileFreshnessCalculator())
|
this.freshnesses.set(id, new TileFreshnessCalculator())
|
||||||
|
|
||||||
if (id === "type_node") {
|
if (id === "type_node") {
|
||||||
// Handles by the 'FullNodeDatabaseSource'
|
|
||||||
|
this.fullNodeDatabase = new FullNodeDatabaseSource(
|
||||||
|
filteredLayer,
|
||||||
|
tile => {
|
||||||
|
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
||||||
|
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
||||||
|
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
||||||
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -248,17 +261,8 @@ export default class FeaturePipeline {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if(this.fullNodeDatabase !== undefined){
|
||||||
if (state.layoutToUse.trackAllNodes) {
|
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => this.fullNodeDatabase.handleOsmJson(osmJson, tileId))
|
||||||
const fullNodeDb = new FullNodeDatabaseSource(
|
|
||||||
state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0],
|
|
||||||
tile => {
|
|
||||||
new RegisteringAllFromFeatureSourceActor(tile, state.allElements)
|
|
||||||
perLayerHierarchy.get(tile.layer.layerDef.id).registerTile(tile)
|
|
||||||
tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile))
|
|
||||||
})
|
|
||||||
|
|
||||||
osmFeatureSource.rawDataHandlers.push((osmJson, tileId) => fullNodeDb.handleOsmJson(osmJson, tileId))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
||||||
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
|
public readonly loadedTiles = new Map<number, FeatureSource & Tiled>()
|
||||||
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
|
private readonly onTileLoaded: (tile: (Tiled & FeatureSourceForLayer)) => void;
|
||||||
private readonly layer: FilteredLayer
|
private readonly layer: FilteredLayer
|
||||||
|
private readonly nodeByIds = new Map<number, OsmNode>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
layer: FilteredLayer,
|
layer: FilteredLayer,
|
||||||
|
@ -31,6 +32,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
||||||
}
|
}
|
||||||
const osmNode = <OsmNode>osmObj;
|
const osmNode = <OsmNode>osmObj;
|
||||||
nodesById.set(osmNode.id, osmNode)
|
nodesById.set(osmNode.id, osmNode)
|
||||||
|
this.nodeByIds.set(osmNode.id, osmNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
const parentWaysByNodeId = new Map<number, OsmWay[]>()
|
const parentWaysByNodeId = new Map<number, OsmWay[]>()
|
||||||
|
@ -49,6 +51,7 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
||||||
}
|
}
|
||||||
parentWaysByNodeId.forEach((allWays, nodeId) => {
|
parentWaysByNodeId.forEach((allWays, nodeId) => {
|
||||||
nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags))
|
nodesById.get(nodeId).tags["parent_ways"] = JSON.stringify(allWays.map(w => w.tags))
|
||||||
|
nodesById.get(nodeId).tags["parent_way_ids"] = JSON.stringify(allWays.map(w => w.id))
|
||||||
})
|
})
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
|
const asGeojsonFeatures = Array.from(nodesById.values()).map(osmNode => ({
|
||||||
|
@ -62,6 +65,16 @@ export default class FullNodeDatabaseSource implements TileHierarchy<FeatureSour
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the OsmNode with the corresponding id (undefined if not found)
|
||||||
|
* Note that this OsmNode will have a calculated tag 'parent_ways' and 'parent_way_ids', which are resp. stringified lists of parent way tags and ids
|
||||||
|
* @param id
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
public GetNode(id: number) : OsmNode {
|
||||||
|
return this.nodeByIds.get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import OsmChangeAction, {OsmCreateAction} from "./OsmChangeAction";
|
import {OsmCreateAction} from "./OsmChangeAction";
|
||||||
import {Tag} from "../../Tags/Tag";
|
import {Tag} from "../../Tags/Tag";
|
||||||
import {Changes} from "../Changes";
|
import {Changes} from "../Changes";
|
||||||
import {ChangeDescription} from "./ChangeDescription";
|
import {ChangeDescription} from "./ChangeDescription";
|
||||||
|
@ -18,10 +18,34 @@ export interface MergePointConfig {
|
||||||
mode: "reuse_osm_point" | "move_osm_point"
|
mode: "reuse_osm_point" | "move_osm_point"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CreateWayWithPointreuse will create a 'CoordinateInfo' for _every_ point in the way to be created.
|
||||||
|
*
|
||||||
|
* The CoordinateInfo indicates the action to take, e.g.:
|
||||||
|
*
|
||||||
|
* - Create a new point
|
||||||
|
* - Reuse an existing OSM point (and don't move it)
|
||||||
|
* - Reuse an existing OSM point (and leave it where it is)
|
||||||
|
* - Reuse another Coordinate info (and don't do anything else with it)
|
||||||
|
*
|
||||||
|
*/
|
||||||
interface CoordinateInfo {
|
interface CoordinateInfo {
|
||||||
|
/**
|
||||||
|
* The new coordinate
|
||||||
|
*/
|
||||||
lngLat: [number, number],
|
lngLat: [number, number],
|
||||||
|
/**
|
||||||
|
* If set: indicates that this point is identical to an earlier point in the way and that that point should be used.
|
||||||
|
* This is especially needed in closed ways, where the last CoordinateInfo will have '0' as identicalTo
|
||||||
|
*/
|
||||||
identicalTo?: number,
|
identicalTo?: number,
|
||||||
|
/**
|
||||||
|
* Information about the closebyNode which might be reused
|
||||||
|
*/
|
||||||
closebyNodes?: {
|
closebyNodes?: {
|
||||||
|
/**
|
||||||
|
* Distance in meters between the target coordinate and this candidate coordinate
|
||||||
|
*/
|
||||||
d: number,
|
d: number,
|
||||||
node: any,
|
node: any,
|
||||||
config: MergePointConfig
|
config: MergePointConfig
|
||||||
|
@ -53,6 +77,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
this._tags = tags;
|
this._tags = tags;
|
||||||
this._state = state;
|
this._state = state;
|
||||||
this._config = config;
|
this._config = config;
|
||||||
|
|
||||||
|
// The main logic of this class: the coordinateInfo contains all the changes
|
||||||
this._coordinateInfo = this.CalculateClosebyNodes(coordinates);
|
this._coordinateInfo = this.CalculateClosebyNodes(coordinates);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -219,6 +245,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
return allChanges
|
return allChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the main changes.
|
||||||
|
*/
|
||||||
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
|
private CalculateClosebyNodes(coordinates: [number, number][]): CoordinateInfo[] {
|
||||||
|
|
||||||
const bbox = new BBox(coordinates)
|
const bbox = new BBox(coordinates)
|
||||||
|
@ -226,6 +255,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2)))
|
const allNodes = [].concat(...state.featurePipeline.GetFeaturesWithin("type_node", bbox.pad(1.2)))
|
||||||
const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM))
|
const maxDistance = Math.max(...this._config.map(c => c.withinRangeOfM))
|
||||||
|
|
||||||
|
// Init coordianteinfo with undefined but the same length as coordinates
|
||||||
const coordinateInfo: {
|
const coordinateInfo: {
|
||||||
lngLat: [number, number],
|
lngLat: [number, number],
|
||||||
identicalTo?: number,
|
identicalTo?: number,
|
||||||
|
@ -236,6 +266,8 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
}[]
|
}[]
|
||||||
}[] = coordinates.map(_ => undefined)
|
}[] = coordinates.map(_ => undefined)
|
||||||
|
|
||||||
|
|
||||||
|
// First loop: gather all information...
|
||||||
for (let i = 0; i < coordinates.length; i++) {
|
for (let i = 0; i < coordinates.length; i++) {
|
||||||
|
|
||||||
if (coordinateInfo[i] !== undefined) {
|
if (coordinateInfo[i] !== undefined) {
|
||||||
|
@ -243,8 +275,11 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
const coor = coordinates[i]
|
const coor = coordinates[i]
|
||||||
// Check closeby (and probably identical) point further in the coordinate list, mark them as duplicate
|
// Check closeby (and probably identical) points further in the coordinate list, mark them as duplicate
|
||||||
for (let j = i + 1; j < coordinates.length; j++) {
|
for (let j = i + 1; j < coordinates.length; j++) {
|
||||||
|
// We look into the 'future' of the way and mark those 'future' locations as being the same as this location
|
||||||
|
// The continue just above will make sure they get ignored
|
||||||
|
// This code is important to 'close' ways
|
||||||
if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
|
if (GeoOperations.distanceBetween(coor, coordinates[j]) < 0.1) {
|
||||||
coordinateInfo[j] = {
|
coordinateInfo[j] = {
|
||||||
lngLat: coor,
|
lngLat: coor,
|
||||||
|
@ -280,6 +315,7 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort by distance, closest first
|
||||||
closebyNodes.sort((n0, n1) => {
|
closebyNodes.sort((n0, n1) => {
|
||||||
return n0.d - n1.d
|
return n0.d - n1.d
|
||||||
})
|
})
|
||||||
|
@ -292,8 +328,9 @@ export default class CreateWayWithPointReuseAction extends OsmCreateAction {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let conflictFree = true;
|
|
||||||
|
|
||||||
|
// Second loop: figure out which point moves where without creating conflicts
|
||||||
|
let conflictFree = true;
|
||||||
do {
|
do {
|
||||||
conflictFree = true;
|
conflictFree = true;
|
||||||
for (let i = 0; i < coordinateInfo.length; i++) {
|
for (let i = 0; i < coordinateInfo.length; i++) {
|
||||||
|
|
|
@ -37,7 +37,7 @@ export default class DeleteAction extends OsmChangeAction {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
public async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||||
|
|
||||||
const osmObject = await OsmObject.DownloadObjectAsync(this._id)
|
const osmObject = await OsmObject.DownloadObjectAsync(this._id)
|
||||||
|
|
||||||
|
|
|
@ -11,28 +11,36 @@ import ChangeTagAction from "./ChangeTagAction";
|
||||||
import {And} from "../../Tags/And";
|
import {And} from "../../Tags/And";
|
||||||
import {Utils} from "../../../Utils";
|
import {Utils} from "../../../Utils";
|
||||||
import {OsmConnection} from "../OsmConnection";
|
import {OsmConnection} from "../OsmConnection";
|
||||||
|
import {GeoJSONObject} from "@turf/turf";
|
||||||
|
import FeaturePipeline from "../../FeatureSource/FeaturePipeline";
|
||||||
|
import DeleteAction from "./DeleteAction";
|
||||||
|
|
||||||
export default class ReplaceGeometryAction extends OsmChangeAction {
|
export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
|
/**
|
||||||
|
* The target feature - mostly used for the metadata
|
||||||
|
*/
|
||||||
private readonly feature: any;
|
private readonly feature: any;
|
||||||
private readonly state: {
|
private readonly state: {
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection,
|
||||||
|
featurePipeline: FeaturePipeline
|
||||||
};
|
};
|
||||||
private readonly wayToReplaceId: string;
|
private readonly wayToReplaceId: string;
|
||||||
private readonly theme: string;
|
private readonly theme: string;
|
||||||
/**
|
/**
|
||||||
* The target coordinates that should end up in OpenStreetMap
|
* The target coordinates that should end up in OpenStreetMap.
|
||||||
|
* This is identical to either this.feature.geometry.coordinates or -in case of a polygon- feature.geometry.coordinates[0]
|
||||||
*/
|
*/
|
||||||
private readonly targetCoordinates: [number, number][];
|
private readonly targetCoordinates: [number, number][];
|
||||||
/**
|
/**
|
||||||
* If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index.
|
* If a target coordinate is close to another target coordinate, 'identicalTo' will point to the first index.
|
||||||
* @private
|
|
||||||
*/
|
*/
|
||||||
private readonly identicalTo: number[]
|
private readonly identicalTo: number[]
|
||||||
private readonly newTags: Tag[] | undefined;
|
private readonly newTags: Tag[] | undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
state: {
|
state: {
|
||||||
osmConnection: OsmConnection
|
osmConnection: OsmConnection,
|
||||||
|
featurePipeline: FeaturePipeline
|
||||||
},
|
},
|
||||||
feature: any,
|
feature: any,
|
||||||
wayToReplaceId: string,
|
wayToReplaceId: string,
|
||||||
|
@ -54,6 +62,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
} else if (geom.type === "Polygon") {
|
} else if (geom.type === "Polygon") {
|
||||||
coordinates = geom.coordinates[0]
|
coordinates = geom.coordinates[0]
|
||||||
}
|
}
|
||||||
|
this.targetCoordinates = coordinates
|
||||||
|
|
||||||
this.identicalTo = coordinates.map(_ => undefined)
|
this.identicalTo = coordinates.map(_ => undefined)
|
||||||
|
|
||||||
|
@ -68,21 +77,18 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
this.targetCoordinates = coordinates
|
|
||||||
this.newTags = options.newTags
|
this.newTags = options.newTags
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedGlobalSymbols
|
||||||
public async getPreview(): Promise<FeatureSource> {
|
public async getPreview(): Promise<FeatureSource> {
|
||||||
const {closestIds, allNodesById} = await this.GetClosestIds();
|
const {closestIds, allNodesById, detachedNodeIds} = await this.GetClosestIds();
|
||||||
console.debug("Generating preview, identicals are ",)
|
console.debug("Generating preview, identicals are ",)
|
||||||
const preview = closestIds.map((newId, i) => {
|
const preview: GeoJSONObject[] = closestIds.map((newId, i) => {
|
||||||
if (this.identicalTo[i] !== undefined) {
|
if (this.identicalTo[i] !== undefined) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (newId === undefined) {
|
if (newId === undefined) {
|
||||||
return {
|
return {
|
||||||
type: "Feature",
|
type: "Feature",
|
||||||
|
@ -110,6 +116,24 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|
||||||
|
for (const detachedNodeId of detachedNodeIds) {
|
||||||
|
const origPoint = allNodesById.get(detachedNodeId).centerpoint()
|
||||||
|
const feature = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {
|
||||||
|
"detach": "yes",
|
||||||
|
"id": "replace-geometry-detach-" + detachedNodeId
|
||||||
|
},
|
||||||
|
geometry: {
|
||||||
|
type: "Point",
|
||||||
|
coordinates: [origPoint[1], origPoint[0]]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
preview.push(feature)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
return new StaticFeatureSource(Utils.NoNull(preview), false)
|
return new StaticFeatureSource(Utils.NoNull(preview), false)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -119,7 +143,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
const allChanges: ChangeDescription[] = []
|
const allChanges: ChangeDescription[] = []
|
||||||
const actualIdsToUse: number[] = []
|
const actualIdsToUse: number[] = []
|
||||||
|
|
||||||
const {closestIds, osmWay} = await this.GetClosestIds()
|
const {closestIds, osmWay, detachedNodeIds} = await this.GetClosestIds()
|
||||||
|
|
||||||
for (let i = 0; i < closestIds.length; i++) {
|
for (let i = 0; i < closestIds.length; i++) {
|
||||||
if (this.identicalTo[i] !== undefined) {
|
if (this.identicalTo[i] !== undefined) {
|
||||||
|
@ -170,7 +194,7 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
|
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// AT the very last: actually change the nodes of the way!
|
// Actually change the nodes of the way!
|
||||||
allChanges.push({
|
allChanges.push({
|
||||||
type: "way",
|
type: "way",
|
||||||
id: osmWay.id,
|
id: osmWay.id,
|
||||||
|
@ -185,6 +209,43 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
// Some nodes might need to be deleted
|
||||||
|
if (detachedNodeIds.length > 0) {
|
||||||
|
|
||||||
|
const nodeDb = this.state.featurePipeline.fullNodeDatabase;
|
||||||
|
if (nodeDb === undefined) {
|
||||||
|
throw "PANIC: replaceGeometryAction needs the FullNodeDatabase, which is undefined. This should be initialized by having the 'type_node'-layer enabled in your theme. (NB: the replacebutton has type_node as dependency)"
|
||||||
|
}
|
||||||
|
for (const nodeId of detachedNodeIds) {
|
||||||
|
const osmNode = nodeDb.GetNode(nodeId)
|
||||||
|
const parentWayIds: number[] = JSON.parse(osmNode.tags["parent_way_ids"])
|
||||||
|
const index = parentWayIds.indexOf(osmWay.id)
|
||||||
|
if(index < 0){
|
||||||
|
console.error("ReplaceGeometryAction is trying to detach node "+nodeId+", but it isn't listed as being part of way "+osmWay.id)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
parentWayIds.splice(index, 1)
|
||||||
|
osmNode.tags["parent_way_ids"] = JSON.stringify(parentWayIds)
|
||||||
|
if(parentWayIds.length == 0){
|
||||||
|
// This point has no other ways anymore - lets clean it!
|
||||||
|
console.log("Removing node "+nodeId, "as it isn't needed anymore by any way")
|
||||||
|
|
||||||
|
allChanges.push({
|
||||||
|
meta: {
|
||||||
|
theme: this.theme,
|
||||||
|
changeType: "delete"
|
||||||
|
},
|
||||||
|
doDelete: true,
|
||||||
|
type: "node",
|
||||||
|
id: nodeId,
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return allChanges
|
return allChanges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,10 +254,21 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
* @constructor
|
* @constructor
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> {
|
private async GetClosestIds(): Promise<{
|
||||||
|
|
||||||
|
// A list of the same length as targetCoordinates, containing which OSM-point to move. If undefined, a new point will be created
|
||||||
|
closestIds: number[],
|
||||||
|
allNodesById: Map<number, OsmNode>,
|
||||||
|
osmWay: OsmWay,
|
||||||
|
detachedNodeIds: number[]
|
||||||
|
}> {
|
||||||
// TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them)
|
// 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: if a new point has to be created, snap to already existing ways
|
||||||
// TODO FIXME: detect intersections with other ways if moved
|
|
||||||
|
|
||||||
|
let parsed: OsmObject[];
|
||||||
|
{
|
||||||
|
// Gather the needed OsmObjects
|
||||||
const splitted = this.wayToReplaceId.split("/");
|
const splitted = this.wayToReplaceId.split("/");
|
||||||
const type = splitted[0];
|
const type = splitted[0];
|
||||||
const idN = Number(splitted[1]);
|
const idN = Number(splitted[1]);
|
||||||
|
@ -205,72 +277,101 @@ export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
|
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
|
||||||
const rawData = await Utils.downloadJsonCached(url, 1000)
|
const rawData = await Utils.downloadJsonCached(url, 1000)
|
||||||
const parsed = OsmObject.ParseObjects(rawData.elements);
|
parsed = OsmObject.ParseObjects(rawData.elements);
|
||||||
const allNodesById = new Map<number, OsmNode>()
|
|
||||||
const allNodes = parsed.filter(o => o.type === "node")
|
|
||||||
for (const node of allNodes) {
|
|
||||||
allNodesById.set(node.id, <OsmNode>node)
|
|
||||||
}
|
}
|
||||||
|
const allNodes = parsed.filter(o => o.type === "node")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allright! We know all the nodes of the original way and all the nodes of the target coordinates.
|
* For every already existing OSM-point, we calculate the distance to every target point
|
||||||
* For each of the target coordinates, we search the closest, already existing point and reuse this point
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const closestIds = []
|
const distances = new Map<number /* osmId*/, number[] /* target coordinate index --> distance (or undefined if a duplicate)*/>();
|
||||||
const distances = []
|
for (const node of allNodes) {
|
||||||
|
const nodeDistances = this.targetCoordinates.map(_ => undefined)
|
||||||
for (let i = 0; i < this.targetCoordinates.length; i++) {
|
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) {
|
|
||||||
|
|
||||||
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
|
|
||||||
// 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) {
|
if (this.identicalTo[i] !== undefined) {
|
||||||
closestIds[i] = closestIds[this.identicalTo[i]]
|
continue;
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
const closestId = closestIds[i]
|
const targetCoordinate = this.targetCoordinates[i];
|
||||||
for (let j = i + 1; j < closestIds.length; j++) {
|
const cp = node.centerpoint()
|
||||||
if (this.identicalTo[j] !== undefined) {
|
nodeDistances[i] = GeoOperations.distanceBetween(targetCoordinate, [cp[1], cp[0]])
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
const otherClosestId = closestIds[j]
|
distances.set(node.id, nodeDistances)
|
||||||
if (closestId !== otherClosestId) {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// We have two occurences of 'closestId' - we only keep the closest instance!
|
|
||||||
const di = distances[i]
|
/**
|
||||||
const dj = distances[j]
|
* Then, we search the node that has to move the least distance and add this as mapping.
|
||||||
if (di < dj) {
|
* We do this until no points are left
|
||||||
closestIds[j] = undefined
|
*/
|
||||||
|
let candidate: number;
|
||||||
|
let moveDistance: number;
|
||||||
|
const closestIds = this.targetCoordinates.map(_ => undefined)
|
||||||
|
/**
|
||||||
|
* The list of nodes that are _not_ used anymore, typically if there are less targetCoordinates then source coordinates
|
||||||
|
*/
|
||||||
|
const unusedIds = []
|
||||||
|
do {
|
||||||
|
candidate = undefined;
|
||||||
|
moveDistance = Infinity;
|
||||||
|
distances.forEach((distances, nodeId) => {
|
||||||
|
const minDist = Math.min(...Utils.NoNull(distances))
|
||||||
|
if (moveDistance > minDist) {
|
||||||
|
// We have found a candidate to move
|
||||||
|
candidate = nodeId
|
||||||
|
moveDistance = minDist
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (candidate !== undefined) {
|
||||||
|
// We found a candidate... Search the corresponding target id:
|
||||||
|
let targetId: number = undefined;
|
||||||
|
let lowestDistance = Number.MAX_VALUE
|
||||||
|
let nodeDistances = distances.get(candidate)
|
||||||
|
for (let i = 0; i < nodeDistances.length; i++) {
|
||||||
|
const d = nodeDistances[i]
|
||||||
|
if (d !== undefined && d < lowestDistance) {
|
||||||
|
lowestDistance = d;
|
||||||
|
targetId = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This candidates role is done, it can be removed from the distance matrix
|
||||||
|
distances.delete(candidate)
|
||||||
|
|
||||||
|
if (targetId !== undefined) {
|
||||||
|
// At this point, we have our target coordinate index: targetId!
|
||||||
|
// Lets map it...
|
||||||
|
closestIds[targetId] = candidate
|
||||||
|
|
||||||
|
// To indicate that this targetCoordinate is taken, we remove them from the distances matrix
|
||||||
|
distances.forEach(dists => {
|
||||||
|
dists[targetId] = undefined
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
closestIds[i] = undefined
|
// Seems like all the targetCoordinates have found a source point
|
||||||
}
|
unusedIds.push(candidate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} while (candidate !== undefined)
|
||||||
|
|
||||||
|
|
||||||
|
// If there are still unused values in 'distances', they are definitively unused
|
||||||
|
distances.forEach((_, nodeId) => {
|
||||||
|
unusedIds.push(nodeId)
|
||||||
|
})
|
||||||
|
|
||||||
|
{
|
||||||
|
// Some extra data is included for rendering
|
||||||
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
||||||
if (osmWay.type !== "way") {
|
if (osmWay.type !== "way") {
|
||||||
throw "WEIRD: expected an OSM-way as last element here!"
|
throw "WEIRD: expected an OSM-way as last element here!"
|
||||||
}
|
}
|
||||||
return {closestIds, allNodesById, osmWay};
|
const allNodesById = new Map<number, OsmNode>()
|
||||||
|
for (const node of allNodes) {
|
||||||
|
allNodesById.set(node.id, <OsmNode>node)
|
||||||
|
}
|
||||||
|
return {closestIds, allNodesById, osmWay, detachedNodeIds: unusedIds};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -384,8 +384,8 @@ export class Changes {
|
||||||
states.set(o.type + "/" + o.id, "unchanged")
|
states.set(o.type + "/" + o.id, "unchanged")
|
||||||
}
|
}
|
||||||
|
|
||||||
let changed = false;
|
|
||||||
for (const change of changes) {
|
for (const change of changes) {
|
||||||
|
let changed = false;
|
||||||
const id = change.type + "/" + change.id
|
const id = change.type + "/" + change.id
|
||||||
if (!objects.has(id)) {
|
if (!objects.has(id)) {
|
||||||
// The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition
|
// The object hasn't been seen before, so it doesn't exist yet and is newly created by its very definition
|
||||||
|
@ -493,7 +493,7 @@ export class Changes {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed && state === "unchanged") {
|
if (changed && states.get(id) === "unchanged") {
|
||||||
states.set(id, "modified")
|
states.set(id, "modified")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -520,6 +520,7 @@ export class Changes {
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
console.debug("Calculated the pending changes: ", result.newObjects.length,"new; ", result.modifiedObjects.length,"modified;",result.deletedObjects,"deleted")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,7 +2,7 @@ import {Utils} from "../Utils";
|
||||||
|
|
||||||
export default class Constants {
|
export default class Constants {
|
||||||
|
|
||||||
public static vNumber = "0.13.0-alpha-8";
|
public static vNumber = "0.13.0-alpha-9";
|
||||||
public static ImgurApiKey = '7070e7167f0a25a'
|
public static ImgurApiKey = '7070e7167f0a25a'
|
||||||
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85"
|
||||||
|
|
||||||
|
|
|
@ -63,7 +63,6 @@ export default class LayoutConfig {
|
||||||
this.credits = json.credits;
|
this.credits = json.credits;
|
||||||
this.version = json.version;
|
this.version = json.version;
|
||||||
this.language = [];
|
this.language = [];
|
||||||
this.trackAllNodes = false
|
|
||||||
|
|
||||||
if (typeof json.language === "string") {
|
if (typeof json.language === "string") {
|
||||||
this.language = [json.language];
|
this.language = [json.language];
|
||||||
|
@ -101,7 +100,6 @@ export default class LayoutConfig {
|
||||||
this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
|
this.tileLayerSources = (json.tileLayerSources ?? []).map((config, i) => new TilesourceConfig(config, `${this.id}.tileLayerSources[${i}]`))
|
||||||
const layerInfo = LayoutConfig.ExtractLayers(json, official, context);
|
const layerInfo = LayoutConfig.ExtractLayers(json, official, context);
|
||||||
this.layers = layerInfo.layers
|
this.layers = layerInfo.layers
|
||||||
this.trackAllNodes = layerInfo.extractAllNodes
|
|
||||||
|
|
||||||
|
|
||||||
this.clustering = {
|
this.clustering = {
|
||||||
|
|
|
@ -232,7 +232,7 @@ ${Utils.special_visualizations_importRequirementDocs}
|
||||||
onCancel: () => void): BaseUIElement {
|
onCancel: () => void): BaseUIElement {
|
||||||
const self = this;
|
const self = this;
|
||||||
const confirmationMap = Minimap.createMiniMap({
|
const confirmationMap = Minimap.createMiniMap({
|
||||||
allowMoving: false,
|
allowMoving: state.featureSwitchIsDebugging.data ?? false,
|
||||||
background: state.backgroundLayer
|
background: state.backgroundLayer
|
||||||
})
|
})
|
||||||
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
confirmationMap.SetStyle("height: 20rem; overflow: hidden").SetClass("rounded-xl")
|
||||||
|
@ -297,6 +297,13 @@ export class ConflateButton extends AbstractImportButton {
|
||||||
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
return feature.geometry.type === "LineString" || (feature.geometry.type === "Polygon" && feature.geometry.coordinates.length === 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLayerDependencies(argsRaw: string[]): string[] {
|
||||||
|
const deps = super.getLayerDependencies(argsRaw);
|
||||||
|
// Force 'type_node' as dependency
|
||||||
|
deps.push("type_node")
|
||||||
|
return deps;
|
||||||
|
}
|
||||||
|
|
||||||
constructElement(state: FeaturePipelineState,
|
constructElement(state: FeaturePipelineState,
|
||||||
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
|
args: { max_snap_distance: string; snap_onto_layers: string; icon: string; text: string; tags: string; newTags: UIEventSource<Tag[]>; targetLayer: string },
|
||||||
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement {
|
tagSource: UIEventSource<any>, guiState: DefaultGuiState, feature: any, onCancelClicked: () => void): BaseUIElement {
|
||||||
|
|
|
@ -15,7 +15,15 @@
|
||||||
"mapRendering": [
|
"mapRendering": [
|
||||||
{
|
{
|
||||||
"location": "point",
|
"location": "point",
|
||||||
"icon": "addSmall:#000",
|
"icon": {
|
||||||
|
"render": "addSmall:#000",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "detach=yes",
|
||||||
|
"then": "circle:white;close:#c33"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"iconSize": "10,10,center"
|
"iconSize": "10,10,center"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -302,6 +302,25 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "service_ways",
|
||||||
|
"name": "Service roads",
|
||||||
|
"description": "A seperate layer with service roads, as to remove them from the intersection testing",
|
||||||
|
"source": {
|
||||||
|
"osmTags": "highway=service"
|
||||||
|
},
|
||||||
|
"mapRendering": [
|
||||||
|
{
|
||||||
|
"width": 4,
|
||||||
|
"color": "#888888"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"title": {
|
||||||
|
"render": "Service road"
|
||||||
|
},
|
||||||
|
"tagRenderings": []
|
||||||
|
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "generic_osm_object",
|
"id": "generic_osm_object",
|
||||||
"name": "All OSM Objects",
|
"name": "All OSM Objects",
|
||||||
|
@ -466,7 +485,7 @@
|
||||||
"name": "GRB geometries",
|
"name": "GRB geometries",
|
||||||
"title": "GRB outline",
|
"title": "GRB outline",
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_overlaps_with_buildings=feat.overlapWith('OSM-buildings')",
|
"_overlaps_with_buildings=feat.overlapWith('OSM-buildings').filter(f => f.feat.properties.id.indexOf('-') < 0)",
|
||||||
"_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''",
|
"_overlaps_with=feat.get('_overlaps_with_buildings').filter(f => f.overlap > 1 /* square meter */ )[0] ?? ''",
|
||||||
"_overlap_absolute=feat.get('_overlaps_with')?.overlap",
|
"_overlap_absolute=feat.get('_overlaps_with')?.overlap",
|
||||||
"_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ",
|
"_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ",
|
||||||
|
@ -551,6 +570,12 @@
|
||||||
"_osm_obj:id~*",
|
"_osm_obj:id~*",
|
||||||
"addr:street~*",
|
"addr:street~*",
|
||||||
"addr:housenumber~*",
|
"addr:housenumber~*",
|
||||||
|
{
|
||||||
|
"or": [
|
||||||
|
"addr:street~*",
|
||||||
|
"addr:housenumber~*"
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"or": [
|
"or": [
|
||||||
"addr:street!:={_osm_obj:addr:street}",
|
"addr:street!:={_osm_obj:addr:street}",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue