forked from MapComplete/MapComplete
Refactoring of GPS-location (uses featureSource too now), factoring out state, add ReplaceGeometryAction and conflation example
This commit is contained in:
parent
1db54f3c8e
commit
2484848cd6
37 changed files with 1035 additions and 467 deletions
|
@ -11,7 +11,7 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
|
||||
constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any, meta: {
|
||||
theme: string,
|
||||
changeType: "answer" | "soft-delete" | "add-image"
|
||||
changeType: "answer" | "soft-delete" | "add-image" | string
|
||||
}) {
|
||||
super();
|
||||
this._elementId = elementId;
|
||||
|
@ -27,11 +27,16 @@ export default class ChangeTagAction extends OsmChangeAction {
|
|||
const key = kv.k;
|
||||
const value = kv.v;
|
||||
if (key === undefined || key === null) {
|
||||
console.log("Invalid key");
|
||||
console.error("Invalid key:", key);
|
||||
return undefined;
|
||||
}
|
||||
if (value === undefined || value === null) {
|
||||
console.log("Invalid value for ", key);
|
||||
console.error("Invalid value for ", key,":", value);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if(typeof value !== "string"){
|
||||
console.error("Invalid value for ", key, "as it is not a string:", value)
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,39 +4,25 @@ import {Changes} from "../Changes";
|
|||
import {Tag} from "../../Tags/Tag";
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction";
|
||||
import {And} from "../../Tags/And";
|
||||
import {TagsFilter} from "../../Tags/TagsFilter";
|
||||
|
||||
export default class CreateNewWayAction extends OsmChangeAction {
|
||||
public newElementId: string = undefined
|
||||
private readonly coordinates: ({ nodeId?: number, lat: number, lon: number })[];
|
||||
private readonly tags: Tag[];
|
||||
private readonly _options: {
|
||||
theme: string, existingPointHandling?: {
|
||||
withinRangeOfM: number,
|
||||
ifMatches?: TagsFilter,
|
||||
mode: "reuse_osm_point" | "move_osm_point"
|
||||
} []
|
||||
theme: string
|
||||
};
|
||||
|
||||
|
||||
/***
|
||||
* Creates a new way to upload to OSM
|
||||
* @param tags: the tags to apply to the wya
|
||||
* @param tags: the tags to apply to the way
|
||||
* @param coordinates: the coordinates. Might have a nodeId, in this case, this node will be used
|
||||
* @param options
|
||||
*/
|
||||
constructor(tags: Tag[], coordinates: ({ nodeId?: number, lat: number, lon: number })[],
|
||||
options: {
|
||||
theme: string,
|
||||
/**
|
||||
* IF specified, an existing OSM-point within this range and satisfying the condition 'ifMatches' will be used instead of a new coordinate.
|
||||
* If multiple points are possible, only the closest point is considered
|
||||
*/
|
||||
existingPointHandling?: {
|
||||
withinRangeOfM: number,
|
||||
ifMatches?: TagsFilter,
|
||||
mode: "reuse_osm_point" | "move_osm_point"
|
||||
} []
|
||||
theme: string
|
||||
}) {
|
||||
super()
|
||||
this.coordinates = coordinates;
|
||||
|
|
232
Logic/Osm/Actions/ReplaceGeometryAction.ts
Normal file
232
Logic/Osm/Actions/ReplaceGeometryAction.ts
Normal file
|
@ -0,0 +1,232 @@
|
|||
import OsmChangeAction from "./OsmChangeAction";
|
||||
import {Changes} from "../Changes";
|
||||
import {ChangeDescription} from "./ChangeDescription";
|
||||
import {Tag} from "../../Tags/Tag";
|
||||
import FeatureSource from "../../FeatureSource/FeatureSource";
|
||||
import {OsmNode, OsmObject, OsmWay} from "../OsmObject";
|
||||
import {GeoOperations} from "../../GeoOperations";
|
||||
import StaticFeatureSource from "../../FeatureSource/Sources/StaticFeatureSource";
|
||||
import CreateNewNodeAction from "./CreateNewNodeAction";
|
||||
import ChangeTagAction from "./ChangeTagAction";
|
||||
import {And} from "../../Tags/And";
|
||||
import {Utils} from "../../../Utils";
|
||||
import {OsmConnection} from "../OsmConnection";
|
||||
|
||||
export default class ReplaceGeometryAction extends OsmChangeAction {
|
||||
private readonly feature: any;
|
||||
private readonly state: {
|
||||
osmConnection: OsmConnection
|
||||
};
|
||||
private readonly wayToReplaceId: string;
|
||||
private readonly theme: string;
|
||||
private readonly targetCoordinates: [number, number][];
|
||||
private readonly newTags: Tag[] | undefined;
|
||||
|
||||
constructor(
|
||||
state: {
|
||||
osmConnection: OsmConnection
|
||||
},
|
||||
feature: any,
|
||||
wayToReplaceId: string,
|
||||
options: {
|
||||
theme: string,
|
||||
newTags?: Tag[]
|
||||
}
|
||||
) {
|
||||
super();
|
||||
this.state = state;
|
||||
this.feature = feature;
|
||||
this.wayToReplaceId = wayToReplaceId;
|
||||
this.theme = options.theme;
|
||||
|
||||
const geom = this.feature.geometry
|
||||
let coordinates: [number, number][]
|
||||
if (geom.type === "LineString") {
|
||||
coordinates = geom.coordinates
|
||||
} else if (geom.type === "Polygon") {
|
||||
coordinates = geom.coordinates[0]
|
||||
}
|
||||
this.targetCoordinates = coordinates
|
||||
this.newTags = options.newTags
|
||||
}
|
||||
|
||||
public async GetPreview(): Promise<FeatureSource> {
|
||||
const {closestIds, allNodesById} = await this.GetClosestIds();
|
||||
const preview = closestIds.map((newId, i) => {
|
||||
if (newId === undefined) {
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"newpoint": "yes",
|
||||
"id": "replace-geometry-move-" + i
|
||||
},
|
||||
geometry: {
|
||||
type: "Point",
|
||||
coordinates: this.targetCoordinates[i]
|
||||
}
|
||||
};
|
||||
}
|
||||
const origPoint = allNodesById.get(newId).centerpoint()
|
||||
return {
|
||||
type: "Feature",
|
||||
properties: {
|
||||
"move": "yes",
|
||||
"osm-id": newId,
|
||||
"id": "replace-geometry-move-" + i
|
||||
},
|
||||
geometry: {
|
||||
type: "LineString",
|
||||
coordinates: [[origPoint[1], origPoint[0]], this.targetCoordinates[i]]
|
||||
}
|
||||
};
|
||||
})
|
||||
return new StaticFeatureSource(preview, false)
|
||||
|
||||
}
|
||||
|
||||
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
|
||||
|
||||
const allChanges: ChangeDescription[] = []
|
||||
const actualIdsToUse: number[] = []
|
||||
|
||||
const {closestIds, osmWay} = await this.GetClosestIds()
|
||||
|
||||
for (let i = 0; i < closestIds.length; i++) {
|
||||
const closestId = closestIds[i];
|
||||
const [lon, lat] = this.targetCoordinates[i]
|
||||
if (closestId === undefined) {
|
||||
|
||||
const newNodeAction = new CreateNewNodeAction(
|
||||
[],
|
||||
lat, lon,
|
||||
{
|
||||
allowReuseOfPreviouslyCreatedPoints: true,
|
||||
theme: this.theme, changeType: null
|
||||
})
|
||||
const changeDescr = await newNodeAction.CreateChangeDescriptions(changes)
|
||||
allChanges.push(...changeDescr)
|
||||
actualIdsToUse.push(newNodeAction.newElementIdNumber)
|
||||
|
||||
} else {
|
||||
const change = <ChangeDescription>{
|
||||
id: closestId,
|
||||
type: "node",
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "move"
|
||||
},
|
||||
changes: {lon, lat}
|
||||
}
|
||||
actualIdsToUse.push(closestId)
|
||||
allChanges.push(change)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.newTags !== undefined && this.newTags.length > 0) {
|
||||
const addExtraTags = new ChangeTagAction(
|
||||
this.wayToReplaceId,
|
||||
new And(this.newTags),
|
||||
osmWay.tags, {
|
||||
theme: this.theme,
|
||||
changeType: "conflation"
|
||||
}
|
||||
)
|
||||
allChanges.push(...await addExtraTags.CreateChangeDescriptions(changes))
|
||||
}
|
||||
|
||||
// AT the very last: actually change the nodes of the way!
|
||||
allChanges.push({
|
||||
type: "way",
|
||||
id: osmWay.id,
|
||||
changes: {
|
||||
nodes: actualIdsToUse,
|
||||
coordinates: this.targetCoordinates
|
||||
},
|
||||
meta: {
|
||||
theme: this.theme,
|
||||
changeType: "conflation"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
return allChanges
|
||||
}
|
||||
|
||||
/**
|
||||
* For 'this.feature`, gets a corresponding closest node that alreay exsists
|
||||
* @constructor
|
||||
* @private
|
||||
*/
|
||||
private async GetClosestIds(): Promise<{ closestIds: number[], allNodesById: Map<number, OsmNode>, osmWay: OsmWay }> {
|
||||
// TODO FIXME: cap move length on points which are embedded into other ways (ev. disconnect them)
|
||||
// TODO FIXME: if a new point has to be created, snap to already existing ways
|
||||
// TODO FIXME: reuse points if they are the same in the target coordinates
|
||||
const splitted = this.wayToReplaceId.split("/");
|
||||
const type = splitted[0];
|
||||
const idN = Number(splitted[1]);
|
||||
if (idN < 0 || type !== "way") {
|
||||
throw "Invalid ID to conflate: " + this.wayToReplaceId
|
||||
}
|
||||
const url = `${this.state.osmConnection._oauth_config.url}/api/0.6/${this.wayToReplaceId}/full`;
|
||||
const rawData = await Utils.downloadJsonCached(url, 1000)
|
||||
const parsed = OsmObject.ParseObjects(rawData.elements);
|
||||
const allNodesById = new Map<number, OsmNode>()
|
||||
const allNodes = parsed.filter(o => o.type === "node")
|
||||
for (const node of allNodes) {
|
||||
allNodesById.set(node.id, <OsmNode>node)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Allright! We know all the nodes of the original way and all the nodes of the target coordinates.
|
||||
* For each of the target coordinates, we search the closest, already existing point and reuse this point
|
||||
*/
|
||||
|
||||
const closestIds = []
|
||||
const distances = []
|
||||
for (const target of this.targetCoordinates) {
|
||||
let closestDistance = undefined
|
||||
let closestId = undefined;
|
||||
for (const osmNode of allNodes) {
|
||||
|
||||
const cp = osmNode.centerpoint()
|
||||
const d = GeoOperations.distanceBetween(target, [cp[1], cp[0]])
|
||||
if (closestId === undefined || closestDistance > d) {
|
||||
closestId = osmNode.id
|
||||
closestDistance = d
|
||||
}
|
||||
}
|
||||
closestIds.push(closestId)
|
||||
distances.push(closestDistance)
|
||||
}
|
||||
|
||||
// Next step: every closestId can only occur once in the list
|
||||
for (let i = 0; i < closestIds.length; i++) {
|
||||
const closestId = closestIds[i]
|
||||
for (let j = i + 1; j < closestIds.length; j++) {
|
||||
const otherClosestId = closestIds[j]
|
||||
if (closestId !== otherClosestId) {
|
||||
continue
|
||||
}
|
||||
// We have two occurences of 'closestId' - we only keep the closest instance!
|
||||
const di = distances[i]
|
||||
const dj = distances[j]
|
||||
if (di < dj) {
|
||||
closestIds[j] = undefined
|
||||
} else {
|
||||
closestIds[i] = undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const osmWay = <OsmWay>parsed[parsed.length - 1]
|
||||
if (osmWay.type !== "way") {
|
||||
throw "WEIRD: expected an OSM-way as last element here!"
|
||||
}
|
||||
return {closestIds, allNodesById, osmWay};
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -114,7 +114,16 @@ export class Changes {
|
|||
}
|
||||
|
||||
public async applyAction(action: OsmChangeAction): Promise<void> {
|
||||
const changes = await action.Perform(this)
|
||||
this.applyChanges(await action.Perform(this))
|
||||
}
|
||||
|
||||
public async applyActions(actions: OsmChangeAction[]) {
|
||||
for (const action of actions) {
|
||||
await this.applyAction(action)
|
||||
}
|
||||
}
|
||||
|
||||
public applyChanges(changes: ChangeDescription[]) {
|
||||
console.log("Received changes:", changes)
|
||||
this.pendingChanges.data.push(...changes);
|
||||
this.pendingChanges.ping();
|
||||
|
@ -126,6 +135,7 @@ export class Changes {
|
|||
CreateNewNodeAction.registerIdRewrites(mappings)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* UPload the selected changes to OSM.
|
||||
* Returns 'true' if successfull and if they can be removed
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue