Add createNewWay-action, more work on GRB import theme, add import button

This commit is contained in:
Pieter Vander Vennet 2021-10-29 16:38:33 +02:00
parent e4cd93ffb0
commit da65bbbc86
9 changed files with 341 additions and 100 deletions

View file

@ -57,13 +57,14 @@ export class ExtraFunction {
doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. " +
"If the current feature is a point, all features that embed the point are given. " +
"The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" +
"The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list" +
"\n" +
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`",
args: ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
},
(params, feat) => {
return (...layerIds: string[]) => {
const result = []
const result : {feat:any, overlap: number}[]= []
const bbox = BBox.get(feat)
@ -79,6 +80,9 @@ export class ExtraFunction {
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
}
}
result.sort((a, b) => b.overlap - a.overlap)
return result;
}
}

View file

@ -16,7 +16,7 @@ export interface ChangeDescription {
/**
* The type of the change
*/
changeType: "answer" | "create" | "split" | "delete" | "move" | string
changeType: "answer" | "create" | "split" | "delete" | "move" | "import" | string | null
/**
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
*/
@ -51,7 +51,8 @@ export interface ChangeDescription {
lat: number,
lon: number
} | {
// Coordinates are only used for rendering. They should be LAT, LON
/* Coordinates are only used for rendering. They should be LON, LAT
* */
coordinates: [number, number][]
nodes: number[],
} | {

View file

@ -8,20 +8,29 @@ import {GeoOperations} from "../../GeoOperations";
export default class CreateNewNodeAction extends OsmChangeAction {
/**
* Maps previously created points onto their assigned ID, to reuse the point if uplaoded
* "lat,lon" --> id
*/
private static readonly previouslyCreatedPoints = new Map<string, number>()
public newElementId: string = undefined
public newElementIdNumber: number = undefined
private readonly _basicTags: Tag[];
private readonly _lat: number;
private readonly _lon: number;
private readonly _snapOnto: OsmWay;
private readonly _reusePointDistance: number;
private meta: { changeType: "create" | "import"; theme: string };
private readonly _reusePreviouslyCreatedPoint: boolean;
constructor(basicTags: Tag[],
lat: number, lon: number,
options: {
snapOnto?: OsmWay,
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" }) {
allowReuseOfPreviouslyCreatedPoints?: boolean,
snapOnto?: OsmWay,
reusePointWithinMeters?: number,
theme: string, changeType: "create" | "import" | null
}) {
super()
this._basicTags = basicTags;
this._lat = lat;
@ -31,18 +40,47 @@ export default class CreateNewNodeAction extends OsmChangeAction {
}
this._snapOnto = options?.snapOnto;
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0)
this.meta = {
theme: options.theme,
changeType: options.changeType
}
}
public static registerIdRewrites(mappings: Map<string, string>) {
const toAdd: [string, number][] = []
this.previouslyCreatedPoints.forEach((oldId, key) => {
if (!mappings.has("node/" + oldId)) {
return;
}
const newId = Number(mappings.get("node/" + oldId).substr("node/".length))
toAdd.push([key, newId])
})
for (const [key, newId] of toAdd) {
CreateNewNodeAction.previouslyCreatedPoints.set(key, newId)
}
}
async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
if (this._reusePreviouslyCreatedPoint) {
const key = this._lat + "," + this._lon
const prev = CreateNewNodeAction.previouslyCreatedPoints
if (prev.has(key)) {
this.newElementIdNumber = prev.get(key)
this.newElementId = "node/" + this.newElementIdNumber
return []
}
}
const id = changes.getNewID()
const properties = {
id: "node/" + id
}
this.newElementId = "node/" + id
this.setElementId(id)
for (const kv of this._basicTags) {
if (typeof kv.value !== "string") {
throw "Invalid value: don't use a regex in a preset"
@ -84,8 +122,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
}
if (reusedPointId !== undefined) {
console.log("Reusing an existing point:", reusedPointId)
this.newElementId = "node/" + reusedPointId
this.setElementId(reusedPointId)
return [{
tags: new And(this._basicTags).asChange(properties),
type: "node",
@ -112,10 +149,20 @@ export default class CreateNewNodeAction extends OsmChangeAction {
coordinates: locations,
nodes: ids
},
meta:this.meta
meta: this.meta
}
]
}
private setElementId(id: number) {
this.newElementIdNumber = id;
this.newElementId = "node/"+id
if (!this._reusePreviouslyCreatedPoint) {
return
}
const key = this._lat + "," + this._lon
CreateNewNodeAction.previouslyCreatedPoints.set(key, id)
}
}

View file

@ -0,0 +1,74 @@
import {ChangeDescription} from "./ChangeDescription";
import OsmChangeAction from "./OsmChangeAction";
import {Changes} from "../Changes";
import {Tag} from "../../Tags/Tag";
import CreateNewNodeAction from "./CreateNewNodeAction";
import {And} from "../../Tags/And";
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 };
/***
* Creates a new way to upload to OSM
* @param tags: the tags to apply to the wya
* @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
}) {
super()
this.coordinates = coordinates;
this.tags = tags;
this._options = options;
}
protected async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> {
const newElements: ChangeDescription[] = []
const pointIds: number[] = []
for (const coordinate of this.coordinates) {
if (coordinate.nodeId !== undefined) {
pointIds.push(coordinate.nodeId)
continue
}
const newPoint = new CreateNewNodeAction([], coordinate.lat, coordinate.lon, {
changeType: null,
theme: this._options.theme
})
await changes.applyAction(newPoint)
pointIds.push(newPoint.newElementIdNumber)
}
// We have all created (or reused) all the points!
// Time to create the actual way
const id = changes.getNewID()
const newWay = <ChangeDescription> {
id,
type: "way",
meta:{
theme: this._options.theme,
changeType: "import"
},
tags: new And(this.tags).asChange({}),
changes: {
nodes: pointIds,
coordinates: this.coordinates.map(c => [c.lon, c.lat])
}
}
newElements.push(newWay)
this.newElementId = "way/"+id
return newElements
}
}

View file

@ -7,6 +7,7 @@ import {ChangeDescription} from "./Actions/ChangeDescription";
import {Utils} from "../../Utils";
import {LocalStorageSource} from "../Web/LocalStorageSource";
import SimpleMetaTagger from "../SimpleMetaTagger";
import CreateNewNodeAction from "./Actions/CreateNewNodeAction";
/**
* Handles all changes made to OSM.
@ -14,22 +15,20 @@ import SimpleMetaTagger from "../SimpleMetaTagger";
*/
export class Changes {
private _nextId: number = -1; // Newly assigned ID's are negative
public readonly name = "Newly added features"
/**
* All the newly created features as featureSource + all the modified features
*/
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
private _nextId: number = -1; // Newly assigned ID's are negative
private readonly isUploading = new UIEventSource(false);
private readonly previouslyCreated: OsmObject[] = []
private readonly _leftRightSensitive: boolean;
constructor(leftRightSensitive : boolean = false) {
constructor(leftRightSensitive: boolean = false) {
this._leftRightSensitive = leftRightSensitive;
// We keep track of all changes just as well
this.allChanges.setData([...this.pendingChanges.data])
@ -114,21 +113,34 @@ export class Changes {
})
}
public async applyAction(action: OsmChangeAction): Promise<void> {
const changes = await action.Perform(this)
console.log("Received changes:", changes)
this.pendingChanges.data.push(...changes);
this.pendingChanges.ping();
this.allChanges.data.push(...changes)
this.allChanges.ping()
}
public registerIdRewrites(mappings: Map<string, string>): void {
CreateNewNodeAction.registerIdRewrites(mappings)
}
/**
* UPload the selected changes to OSM.
* Returns 'true' if successfull and if they can be removed
* @param pending
* @private
*/
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean>{
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean> {
const self = this;
const neededIds = Changes.GetNeededIds(pending)
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
if(this._leftRightSensitive){
if (this._leftRightSensitive) {
osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags))
}
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
const changes: {
newObjects: OsmObject[],
@ -137,35 +149,38 @@ export class Changes {
} = self.CreateChangesetObjects(pending, osmObjects)
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
console.log("No changes to be made")
return true
return true
}
const meta = pending[0].meta
const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({
key: key,
value: count,
aggregate: true
}))
const perType = Array.from(
Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null)
.map(descr => descr.meta.changeType)), ([key, count]) => (
{
key: key,
value: count,
aggregate: true
}))
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
.map(descr => ({
key: descr.meta.changeType+":"+descr.type+"/"+descr.id,
value: descr.meta.specialMotivation
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
value: descr.meta.specialMotivation
}))
const metatags = [{
key: "comment",
value: "Adding data with #MapComplete for theme #"+meta.theme
value: "Adding data with #MapComplete for theme #" + meta.theme
},
{
key:"theme",
value:meta.theme
key: "theme",
value: meta.theme
},
...perType,
...motivations
]
await State.state.osmConnection.changesetHandler.UploadChangeset(
(csId) => Changes.createChangesetFor(""+csId, changes),
(csId) => Changes.createChangesetFor("" + csId, changes),
metatags
)
@ -178,27 +193,27 @@ export class Changes {
try {
// At last, we build the changeset and upload
const pending = self.pendingChanges.data;
const pendingPerTheme = new Map<string, ChangeDescription[]>()
for (const changeDescription of pending) {
const theme = changeDescription.meta.theme
if(!pendingPerTheme.has(theme)){
if (!pendingPerTheme.has(theme)) {
pendingPerTheme.set(theme, [])
}
pendingPerTheme.get(theme).push(changeDescription)
}
const successes = await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value)
const successes = await Promise.all(Array.from(pendingPerTheme, ([key, value]) => value)
.map(async pendingChanges => {
try{
try {
return await self.flushSelectChanges(pendingChanges);
}catch(e){
console.error("Could not upload some changes:",e)
} catch (e) {
console.error("Could not upload some changes:", e)
return false
}
}))
if(!successes.some(s => s == false)){
if (!successes.some(s => s == false)) {
// All changes successfull, we clear the data!
this.pendingChanges.setData([]);
}
@ -206,22 +221,13 @@ export class Changes {
} catch (e) {
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
self.pendingChanges.setData([])
}finally {
} finally {
self.isUploading.setData(false)
}
}
public async applyAction(action: OsmChangeAction): Promise<void> {
const changes = await action.Perform(this)
console.log("Received changes:", changes)
this.pendingChanges.data.push(...changes);
this.pendingChanges.ping();
this.allChanges.data.push(...changes)
this.allChanges.ping()
}
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
newObjects: OsmObject[],
modifiedObjects: OsmObject[]
@ -373,8 +379,4 @@ export class Changes {
return result
}
public registerIdRewrites(mappings: Map<string, string>): void {
}
}