forked from MapComplete/MapComplete
Add createNewWay-action, more work on GRB import theme, add import button
This commit is contained in:
parent
e4cd93ffb0
commit
da65bbbc86
9 changed files with 341 additions and 100 deletions
|
@ -57,13 +57,14 @@ export class ExtraFunction {
|
||||||
doc: "Gives a list of features from the specified layer which this feature (partly) overlaps with. " +
|
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. " +
|
"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 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" +
|
"\n" +
|
||||||
"For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`",
|
"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)"]
|
args: ["...layerIds - one or more layer ids of the layer from which every feature is checked for overlap)"]
|
||||||
},
|
},
|
||||||
(params, feat) => {
|
(params, feat) => {
|
||||||
return (...layerIds: string[]) => {
|
return (...layerIds: string[]) => {
|
||||||
const result = []
|
const result : {feat:any, overlap: number}[]= []
|
||||||
|
|
||||||
const bbox = BBox.get(feat)
|
const bbox = BBox.get(feat)
|
||||||
|
|
||||||
|
@ -79,6 +80,9 @@ export class ExtraFunction {
|
||||||
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
|
result.push(...GeoOperations.calculateOverlap(feat, otherLayer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result.sort((a, b) => b.overlap - a.overlap)
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,7 +16,7 @@ export interface ChangeDescription {
|
||||||
/**
|
/**
|
||||||
* The type of the change
|
* 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'
|
* THe motivation for the change, e.g. 'deleted because does not exist anymore'
|
||||||
*/
|
*/
|
||||||
|
@ -51,7 +51,8 @@ export interface ChangeDescription {
|
||||||
lat: number,
|
lat: number,
|
||||||
lon: 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][]
|
coordinates: [number, number][]
|
||||||
nodes: number[],
|
nodes: number[],
|
||||||
} | {
|
} | {
|
||||||
|
|
|
@ -8,20 +8,29 @@ import {GeoOperations} from "../../GeoOperations";
|
||||||
|
|
||||||
export default class CreateNewNodeAction extends OsmChangeAction {
|
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 newElementId: string = undefined
|
||||||
|
public newElementIdNumber: number = undefined
|
||||||
private readonly _basicTags: Tag[];
|
private readonly _basicTags: Tag[];
|
||||||
private readonly _lat: number;
|
private readonly _lat: number;
|
||||||
private readonly _lon: number;
|
private readonly _lon: number;
|
||||||
private readonly _snapOnto: OsmWay;
|
private readonly _snapOnto: OsmWay;
|
||||||
private readonly _reusePointDistance: number;
|
private readonly _reusePointDistance: number;
|
||||||
private meta: { changeType: "create" | "import"; theme: string };
|
private meta: { changeType: "create" | "import"; theme: string };
|
||||||
|
private readonly _reusePreviouslyCreatedPoint: boolean;
|
||||||
|
|
||||||
constructor(basicTags: Tag[],
|
constructor(basicTags: Tag[],
|
||||||
lat: number, lon: number,
|
lat: number, lon: number,
|
||||||
options: {
|
options: {
|
||||||
snapOnto?: OsmWay,
|
allowReuseOfPreviouslyCreatedPoints?: boolean,
|
||||||
reusePointWithinMeters?: number,
|
snapOnto?: OsmWay,
|
||||||
theme: string, changeType: "create" | "import" }) {
|
reusePointWithinMeters?: number,
|
||||||
|
theme: string, changeType: "create" | "import" | null
|
||||||
|
}) {
|
||||||
super()
|
super()
|
||||||
this._basicTags = basicTags;
|
this._basicTags = basicTags;
|
||||||
this._lat = lat;
|
this._lat = lat;
|
||||||
|
@ -31,18 +40,47 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
this._snapOnto = options?.snapOnto;
|
this._snapOnto = options?.snapOnto;
|
||||||
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
|
this._reusePointDistance = options?.reusePointWithinMeters ?? 1
|
||||||
|
this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0)
|
||||||
this.meta = {
|
this.meta = {
|
||||||
theme: options.theme,
|
theme: options.theme,
|
||||||
changeType: options.changeType
|
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[]> {
|
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 id = changes.getNewID()
|
||||||
const properties = {
|
const properties = {
|
||||||
id: "node/" + id
|
id: "node/" + id
|
||||||
}
|
}
|
||||||
this.newElementId = "node/" + id
|
this.setElementId(id)
|
||||||
for (const kv of this._basicTags) {
|
for (const kv of this._basicTags) {
|
||||||
if (typeof kv.value !== "string") {
|
if (typeof kv.value !== "string") {
|
||||||
throw "Invalid value: don't use a regex in a preset"
|
throw "Invalid value: don't use a regex in a preset"
|
||||||
|
@ -84,8 +122,7 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
}
|
}
|
||||||
if (reusedPointId !== undefined) {
|
if (reusedPointId !== undefined) {
|
||||||
console.log("Reusing an existing point:", reusedPointId)
|
console.log("Reusing an existing point:", reusedPointId)
|
||||||
this.newElementId = "node/" + reusedPointId
|
this.setElementId(reusedPointId)
|
||||||
|
|
||||||
return [{
|
return [{
|
||||||
tags: new And(this._basicTags).asChange(properties),
|
tags: new And(this._basicTags).asChange(properties),
|
||||||
type: "node",
|
type: "node",
|
||||||
|
@ -112,10 +149,20 @@ export default class CreateNewNodeAction extends OsmChangeAction {
|
||||||
coordinates: locations,
|
coordinates: locations,
|
||||||
nodes: ids
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
74
Logic/Osm/Actions/CreateNewWayAction.ts
Normal file
74
Logic/Osm/Actions/CreateNewWayAction.ts
Normal 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
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ import {ChangeDescription} from "./Actions/ChangeDescription";
|
||||||
import {Utils} from "../../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
import {LocalStorageSource} from "../Web/LocalStorageSource";
|
||||||
import SimpleMetaTagger from "../SimpleMetaTagger";
|
import SimpleMetaTagger from "../SimpleMetaTagger";
|
||||||
|
import CreateNewNodeAction from "./Actions/CreateNewNodeAction";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles all changes made to OSM.
|
* Handles all changes made to OSM.
|
||||||
|
@ -14,22 +15,20 @@ import SimpleMetaTagger from "../SimpleMetaTagger";
|
||||||
*/
|
*/
|
||||||
export class Changes {
|
export class Changes {
|
||||||
|
|
||||||
|
|
||||||
private _nextId: number = -1; // Newly assigned ID's are negative
|
|
||||||
public readonly name = "Newly added features"
|
public readonly name = "Newly added features"
|
||||||
/**
|
/**
|
||||||
* All the newly created features as featureSource + all the modified features
|
* All the newly created features as featureSource + all the modified features
|
||||||
*/
|
*/
|
||||||
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]);
|
||||||
|
|
||||||
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
|
||||||
public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined)
|
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 isUploading = new UIEventSource(false);
|
||||||
|
|
||||||
private readonly previouslyCreated: OsmObject[] = []
|
private readonly previouslyCreated: OsmObject[] = []
|
||||||
private readonly _leftRightSensitive: boolean;
|
private readonly _leftRightSensitive: boolean;
|
||||||
|
|
||||||
constructor(leftRightSensitive : boolean = false) {
|
constructor(leftRightSensitive: boolean = false) {
|
||||||
this._leftRightSensitive = leftRightSensitive;
|
this._leftRightSensitive = leftRightSensitive;
|
||||||
// We keep track of all changes just as well
|
// We keep track of all changes just as well
|
||||||
this.allChanges.setData([...this.pendingChanges.data])
|
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.
|
* UPload the selected changes to OSM.
|
||||||
* Returns 'true' if successfull and if they can be removed
|
* Returns 'true' if successfull and if they can be removed
|
||||||
* @param pending
|
* @param pending
|
||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean>{
|
private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean> {
|
||||||
const self = this;
|
const self = this;
|
||||||
const neededIds = Changes.GetNeededIds(pending)
|
const neededIds = Changes.GetNeededIds(pending)
|
||||||
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
|
const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id)));
|
||||||
|
|
||||||
if(this._leftRightSensitive){
|
if (this._leftRightSensitive) {
|
||||||
osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags))
|
osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags))
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
console.log("Got the fresh objects!", osmObjects, "pending: ", pending)
|
||||||
const changes: {
|
const changes: {
|
||||||
newObjects: OsmObject[],
|
newObjects: OsmObject[],
|
||||||
|
@ -137,35 +149,38 @@ export class Changes {
|
||||||
} = self.CreateChangesetObjects(pending, osmObjects)
|
} = self.CreateChangesetObjects(pending, osmObjects)
|
||||||
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
|
if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) {
|
||||||
console.log("No changes to be made")
|
console.log("No changes to be made")
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const meta = pending[0].meta
|
const meta = pending[0].meta
|
||||||
|
|
||||||
const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({
|
const perType = Array.from(
|
||||||
key: key,
|
Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null)
|
||||||
value: count,
|
.map(descr => descr.meta.changeType)), ([key, count]) => (
|
||||||
aggregate: true
|
{
|
||||||
}))
|
key: key,
|
||||||
|
value: count,
|
||||||
|
aggregate: true
|
||||||
|
}))
|
||||||
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
|
const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined)
|
||||||
.map(descr => ({
|
.map(descr => ({
|
||||||
key: descr.meta.changeType+":"+descr.type+"/"+descr.id,
|
key: descr.meta.changeType + ":" + descr.type + "/" + descr.id,
|
||||||
value: descr.meta.specialMotivation
|
value: descr.meta.specialMotivation
|
||||||
}))
|
}))
|
||||||
const metatags = [{
|
const metatags = [{
|
||||||
key: "comment",
|
key: "comment",
|
||||||
value: "Adding data with #MapComplete for theme #"+meta.theme
|
value: "Adding data with #MapComplete for theme #" + meta.theme
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key:"theme",
|
key: "theme",
|
||||||
value:meta.theme
|
value: meta.theme
|
||||||
},
|
},
|
||||||
...perType,
|
...perType,
|
||||||
...motivations
|
...motivations
|
||||||
]
|
]
|
||||||
|
|
||||||
await State.state.osmConnection.changesetHandler.UploadChangeset(
|
await State.state.osmConnection.changesetHandler.UploadChangeset(
|
||||||
(csId) => Changes.createChangesetFor(""+csId, changes),
|
(csId) => Changes.createChangesetFor("" + csId, changes),
|
||||||
metatags
|
metatags
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -178,27 +193,27 @@ export class Changes {
|
||||||
try {
|
try {
|
||||||
// At last, we build the changeset and upload
|
// At last, we build the changeset and upload
|
||||||
const pending = self.pendingChanges.data;
|
const pending = self.pendingChanges.data;
|
||||||
|
|
||||||
const pendingPerTheme = new Map<string, ChangeDescription[]>()
|
const pendingPerTheme = new Map<string, ChangeDescription[]>()
|
||||||
for (const changeDescription of pending) {
|
for (const changeDescription of pending) {
|
||||||
const theme = changeDescription.meta.theme
|
const theme = changeDescription.meta.theme
|
||||||
if(!pendingPerTheme.has(theme)){
|
if (!pendingPerTheme.has(theme)) {
|
||||||
pendingPerTheme.set(theme, [])
|
pendingPerTheme.set(theme, [])
|
||||||
}
|
}
|
||||||
pendingPerTheme.get(theme).push(changeDescription)
|
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 => {
|
.map(async pendingChanges => {
|
||||||
try{
|
try {
|
||||||
return await self.flushSelectChanges(pendingChanges);
|
return await self.flushSelectChanges(pendingChanges);
|
||||||
}catch(e){
|
} catch (e) {
|
||||||
console.error("Could not upload some changes:",e)
|
console.error("Could not upload some changes:", e)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
if(!successes.some(s => s == false)){
|
if (!successes.some(s => s == false)) {
|
||||||
// All changes successfull, we clear the data!
|
// All changes successfull, we clear the data!
|
||||||
this.pendingChanges.setData([]);
|
this.pendingChanges.setData([]);
|
||||||
}
|
}
|
||||||
|
@ -206,22 +221,13 @@ export class Changes {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
|
console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e)
|
||||||
self.pendingChanges.setData([])
|
self.pendingChanges.setData([])
|
||||||
}finally {
|
} finally {
|
||||||
self.isUploading.setData(false)
|
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[]): {
|
private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): {
|
||||||
newObjects: OsmObject[],
|
newObjects: OsmObject[],
|
||||||
modifiedObjects: OsmObject[]
|
modifiedObjects: OsmObject[]
|
||||||
|
@ -373,8 +379,4 @@ export class Changes {
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public registerIdRewrites(mappings: Map<string, string>): void {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -4,20 +4,34 @@ import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Combine from "../Base/Combine";
|
import Combine from "../Base/Combine";
|
||||||
import {VariableUiElement} from "../Base/VariableUIElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import State from "../../State";
|
|
||||||
import Constants from "../../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
import Toggle from "../Input/Toggle";
|
import Toggle from "../Input/Toggle";
|
||||||
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction";
|
||||||
import {Tag} from "../../Logic/Tags/Tag";
|
import {Tag} from "../../Logic/Tags/Tag";
|
||||||
import Loading from "../Base/Loading";
|
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";
|
||||||
|
import {Changes} from "../../Logic/Osm/Changes";
|
||||||
|
import {ElementStorage} from "../../Logic/ElementStorage";
|
||||||
|
import FeaturePipeline from "../../Logic/FeatureSource/FeaturePipeline";
|
||||||
|
|
||||||
export default class ImportButton extends Toggle {
|
export default class ImportButton extends Toggle {
|
||||||
constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement,
|
constructor(imageUrl: string | BaseUIElement,
|
||||||
|
message: string | BaseUIElement,
|
||||||
originalTags: UIEventSource<any>,
|
originalTags: UIEventSource<any>,
|
||||||
newTags: UIEventSource<Tag[]>,
|
newTags: UIEventSource<Tag[]>,
|
||||||
lat: number, lon: number,
|
feature: any,
|
||||||
minZoom: number,
|
minZoom: number,
|
||||||
state: {
|
state: {
|
||||||
|
featureSwitchUserbadge: UIEventSource<boolean>;
|
||||||
|
featurePipeline: FeaturePipeline;
|
||||||
|
allElements: ElementStorage;
|
||||||
|
selectedElement: UIEventSource<any>;
|
||||||
|
layoutToUse: LayoutConfig,
|
||||||
|
osmConnection: OsmConnection,
|
||||||
|
changes: Changes,
|
||||||
locationControl: UIEventSource<{ zoom: number }>
|
locationControl: UIEventSource<{ zoom: number }>
|
||||||
}) {
|
}) {
|
||||||
const t = Translations.t.general.add;
|
const t = Translations.t.general.add;
|
||||||
|
@ -32,7 +46,7 @@ export default class ImportButton extends Toggle {
|
||||||
const txt = parts.join(" & ")
|
const txt = parts.join(" & ")
|
||||||
return t.presetInfo.Subs({tags: txt}).SetClass("subtle")
|
return t.presetInfo.Subs({tags: txt}).SetClass("subtle")
|
||||||
})), undefined,
|
})), undefined,
|
||||||
State.state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
|
state.osmConnection.userDetails.map(ud => ud.csCount >= Constants.userJourney.tagsVisibleAt)
|
||||||
)
|
)
|
||||||
const button = new SubtleButton(imageUrl, message)
|
const button = new SubtleButton(imageUrl, message)
|
||||||
|
|
||||||
|
@ -44,15 +58,12 @@ export default class ImportButton extends Toggle {
|
||||||
}
|
}
|
||||||
originalTags.data["_imported"] = "yes"
|
originalTags.data["_imported"] = "yes"
|
||||||
originalTags.ping() // will set isImported as per its definition
|
originalTags.ping() // will set isImported as per its definition
|
||||||
const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon, {
|
const newElementAction = ImportButton.createAddActionForFeature(newTags.data, feature, state.layoutToUse.id)
|
||||||
theme: State.state.layoutToUse.id,
|
await state.changes.applyAction(newElementAction)
|
||||||
changeType: "import"
|
state.selectedElement.setData(state.allElements.ContainingFeatures.get(
|
||||||
})
|
|
||||||
await State.state.changes.applyAction(newElementAction)
|
|
||||||
State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get(
|
|
||||||
newElementAction.newElementId
|
newElementAction.newElementId
|
||||||
))
|
))
|
||||||
console.log("Did set selected element to", State.state.allElements.ContainingFeatures.get(
|
console.log("Did set selected element to", state.allElements.ContainingFeatures.get(
|
||||||
newElementAction.newElementId
|
newElementAction.newElementId
|
||||||
))
|
))
|
||||||
|
|
||||||
|
@ -60,25 +71,70 @@ export default class ImportButton extends Toggle {
|
||||||
})
|
})
|
||||||
|
|
||||||
const withLoadingCheck = new Toggle(new Toggle(
|
const withLoadingCheck = new Toggle(new Toggle(
|
||||||
new Loading(t.stillLoading.Clone()),
|
new Loading(t.stillLoading.Clone()),
|
||||||
new Combine([button, appliedTags]).SetClass("flex flex-col"),
|
new Combine([button, appliedTags]).SetClass("flex flex-col"),
|
||||||
State.state.featurePipeline.runningQuery
|
state.featurePipeline.runningQuery
|
||||||
),t.zoomInFurther.Clone(),
|
), t.zoomInFurther.Clone(),
|
||||||
state.locationControl.map(l => l.zoom >= minZoom)
|
state.locationControl.map(l => l.zoom >= minZoom)
|
||||||
)
|
)
|
||||||
const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported)
|
const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported)
|
||||||
|
|
||||||
const pleaseLoginButton =
|
const pleaseLoginButton =
|
||||||
new Toggle(t.pleaseLogin.Clone()
|
new Toggle(t.pleaseLogin.Clone()
|
||||||
.onClick(() => State.state.osmConnection.AttemptLogin())
|
.onClick(() => state.osmConnection.AttemptLogin())
|
||||||
.SetClass("login-button-friendly"),
|
.SetClass("login-button-friendly"),
|
||||||
undefined,
|
undefined,
|
||||||
State.state.featureSwitchUserbadge)
|
state.featureSwitchUserbadge)
|
||||||
|
|
||||||
|
|
||||||
super(importButton,
|
|
||||||
pleaseLoginButton,
|
super(new Toggle(importButton,
|
||||||
State.state.osmConnection.isLoggedIn
|
pleaseLoginButton,
|
||||||
|
state.osmConnection.isLoggedIn
|
||||||
|
),
|
||||||
|
t.wrongType,
|
||||||
|
new UIEventSource(ImportButton.canBeImported(feature))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static canBeImported(feature: any) {
|
||||||
|
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;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -33,6 +33,7 @@ import AllKnownLayers from "../Customizations/AllKnownLayers";
|
||||||
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
|
import ShowDataLayer from "./ShowDataLayer/ShowDataLayer";
|
||||||
import Link from "./Base/Link";
|
import Link from "./Base/Link";
|
||||||
import List from "./Base/List";
|
import List from "./Base/List";
|
||||||
|
import {OsmConnection} from "../Logic/Osm/OsmConnection";
|
||||||
|
|
||||||
export interface SpecialVisualization {
|
export interface SpecialVisualization {
|
||||||
funcName: string,
|
funcName: string,
|
||||||
|
@ -480,7 +481,7 @@ export default class SpecialVisualizations {
|
||||||
args: [
|
args: [
|
||||||
{
|
{
|
||||||
name: "tags",
|
name: "tags",
|
||||||
doc: "Tags to copy-specification. This contains one or more pairs (seperated by a `;`), e.g. `amenity=fast_food; addr:housenumber=$number`. This new point will then have the tags `amenity=fast_food` and `addr:housenumber` with the value that was saved in `number` in the original feature. (Hint: prepare these values, e.g. with calculatedTags)"
|
doc: "The tags to add onto the new object - see specification above"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "text",
|
name: "text",
|
||||||
|
@ -499,6 +500,8 @@ export default class SpecialVisualizations {
|
||||||
}],
|
}],
|
||||||
docs: `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
|
docs: `This button will copy the data from an external dataset into OpenStreetMap. It is only functional in official themes but can be tested in unofficial themes.
|
||||||
|
|
||||||
|
#### Importing a dataset into OpenStreetMap: requirements
|
||||||
|
|
||||||
If you want to import a dataset, make sure that:
|
If you want to import a dataset, make sure that:
|
||||||
|
|
||||||
1. The dataset to import has a suitable license
|
1. The dataset to import has a suitable license
|
||||||
|
@ -507,17 +510,41 @@ If you want to import a dataset, make sure that:
|
||||||
|
|
||||||
There are also some technicalities in your theme to keep in mind:
|
There are also some technicalities in your theme to keep in mind:
|
||||||
|
|
||||||
1. The new point will be added and will flow through the program as any other new point as if it came from OSM.
|
1. The new feature will be added and will flow through the program as any other new point as if it came from OSM.
|
||||||
This means that there should be a layer which will match the new tags and which will display it.
|
This means that there should be a layer which will match the new tags and which will display it.
|
||||||
2. The original point from your geojson layer will gain the tag '_imported=yes'.
|
2. The original feature from your geojson layer will gain the tag '_imported=yes'.
|
||||||
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
|
This should be used to change the appearance or even to hide it (eg by changing the icon size to zero)
|
||||||
3. There should be a way for the theme to detect previously imported points, even after reloading.
|
3. There should be a way for the theme to detect previously imported points, even after reloading.
|
||||||
A reference number to the original dataset is an excellen way to do this
|
A reference number to the original dataset is an excellent way to do this
|
||||||
|
4. When importing ways, the theme creator is also responsible of avoiding overlapping ways.
|
||||||
|
|
||||||
|
#### Disabled in unofficial themes
|
||||||
|
|
||||||
|
The import button can be tested in an unofficial theme by adding \`test=true\` or \`backend=osm-test\` as [URL-paramter](URL_Parameters.md).
|
||||||
|
The import button will show up then. If in testmode, you can read the changeset-XML directly in the web console.
|
||||||
|
In the case that MapComplete is pointed to the testing grounds, the edit will be made on ${OsmConnection.oauth_configs["osm-test"].url}
|
||||||
|
|
||||||
|
|
||||||
|
#### Specifying which tags to copy or add
|
||||||
|
|
||||||
|
The first argument of the import button takes a \`;\`-seperated list of tags to add.
|
||||||
|
|
||||||
|
These can either be a tag to add, such as \`amenity=fast_food\` or can use a substitution, e.g. \`addr:housenumber=$number\`.
|
||||||
|
This new point will then have the tags \`amenity=fast_food\` and \`addr:housenumber\` with the value that was saved in \`number\` in the original feature.
|
||||||
|
|
||||||
|
If a value to substitute is undefined, empty string will be used instead.
|
||||||
|
|
||||||
|
This supports multiple values, e.g. \`ref=$source:geometry:type/$source:geometry:ref\`
|
||||||
|
|
||||||
|
Remark that the syntax is slightly different then expected; it uses '$' to note a value to copy, followed by a name (matched with \`[a-zA-Z0-9_:]*\`). Sadly, delimiting with \`{}\` as these already mark the boundaries of the special rendering...
|
||||||
|
|
||||||
|
Note that these values can be prepare with javascript in the theme by using a [calculatedTag](calculatedTags.md#calculating-tags-with-javascript)
|
||||||
|
|
||||||
`,
|
`,
|
||||||
constr: (state, tagSource, args) => {
|
constr: (state, tagSource, args) => {
|
||||||
if (!state.layoutToUse.official && !state.featureSwitchIsTesting.data) {
|
if (!state.layoutToUse.official && !(state.featureSwitchIsTesting.data || state.osmConnection._oauth_config.url === OsmConnection.oauth_configs["osm-test"].url)) {
|
||||||
return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"),
|
return new Combine([new FixedUiElement("The import button is disabled for unofficial themes to prevent accidents.").SetClass("alert"),
|
||||||
new FixedUiElement("To test, add 'test=true' to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")])
|
new FixedUiElement("To test, add <b>test=true</b> or <b>backend=osm-test</b> to the URL. The changeset will be printed in the console. Please open a PR to officialize this theme to actually enable the import button.")])
|
||||||
}
|
}
|
||||||
const tgsSpec = args[0].split(";").map(spec => {
|
const tgsSpec = args[0].split(";").map(spec => {
|
||||||
const kv = spec.split("=").map(s => s.trim());
|
const kv = spec.split("=").map(s => s.trim());
|
||||||
|
@ -529,9 +556,18 @@ There are also some technicalities in your theme to keep in mind:
|
||||||
const rewrittenTags: UIEventSource<Tag[]> = tagSource.map(tags => {
|
const rewrittenTags: UIEventSource<Tag[]> = tagSource.map(tags => {
|
||||||
const newTags: Tag [] = []
|
const newTags: Tag [] = []
|
||||||
for (const [key, value] of tgsSpec) {
|
for (const [key, value] of tgsSpec) {
|
||||||
if (value.startsWith('$')) {
|
if (value.indexOf('$') >= 0) {
|
||||||
const origKey = value.substring(1)
|
|
||||||
newTags.push(new Tag(key, tags[origKey]))
|
let parts = value.split("$")
|
||||||
|
// THe first of the split won't start with a '$', so no substitution needed
|
||||||
|
let actualValue = parts[0]
|
||||||
|
parts.shift()
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const [_, varName, leftOver] = part.match(/([a-zA-Z0-9_:]*)(.*)/)
|
||||||
|
actualValue += (tags[varName] ?? "") + leftOver
|
||||||
|
}
|
||||||
|
newTags.push(new Tag(key, actualValue))
|
||||||
} else {
|
} else {
|
||||||
newTags.push(new Tag(key, value))
|
newTags.push(new Tag(key, value))
|
||||||
}
|
}
|
||||||
|
@ -540,12 +576,12 @@ There are also some technicalities in your theme to keep in mind:
|
||||||
})
|
})
|
||||||
const id = tagSource.data.id;
|
const id = tagSource.data.id;
|
||||||
const feature = state.allElements.ContainingFeatures.get(id)
|
const feature = state.allElements.ContainingFeatures.get(id)
|
||||||
if (feature.geometry.type !== "Point") {
|
const minzoom = Number(args[3])
|
||||||
return new FixedUiElement("Error: can only import point objects").SetClass("alert")
|
const message = args[1]
|
||||||
}
|
const image = args[2]
|
||||||
const [lon, lat] = feature.geometry.coordinates;
|
|
||||||
return new ImportButton(
|
return new ImportButton(
|
||||||
args[2], args[1], tagSource, rewrittenTags, lat, lon, Number(args[3]), state
|
image, message, tagSource, rewrittenTags, feature, minzoom, state
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -565,16 +565,36 @@
|
||||||
"title": "GRB outline",
|
"title": "GRB outline",
|
||||||
"minzoom": 16,
|
"minzoom": 16,
|
||||||
"calculatedTags": [
|
"calculatedTags": [
|
||||||
"_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 && feat.properties._surface - f.overlap < 5)[0] ?? null",
|
"_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 && (feat.get('_surface') < 20 || f.overlap / feat.get('_surface')) > 0.9)[0] ?? null",
|
||||||
"_osm_obj:source:ref=JSON.parse(feat.properties._overlaps_with)?.feat?.properties['source:geometry:ref']",
|
"_overlap_absolute=feat.get('_overlaps_with')?.overlap",
|
||||||
"_osm_obj:source:date=JSON.parse(feat.properties._overlaps_with)?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')",
|
"_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ",
|
||||||
"_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']",
|
"_osm_obj:source:ref=feat.get('_overlaps_with')?.feat?.properties['source:geometry:ref']",
|
||||||
|
"_osm_obj:source:date=feat.get('_overlaps_with')?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')",
|
||||||
|
"_osm_obj:building=feat.get('_overlaps_with')?.feat?.properties.building",
|
||||||
|
"_osm_obj:id=feat.get('_overlaps_with')?.feat?.properties.id",
|
||||||
|
"_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,'-')",
|
"_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"
|
||||||
],
|
],
|
||||||
"tagRenderings": [
|
"tagRenderings": [
|
||||||
{
|
{
|
||||||
"render": "{import_button(Upload this geometry to OpenStreetMap)}"
|
"id": "Building info",
|
||||||
|
"render": "This is a <b>{building}</b> <span class='subtle'>detected by {detection_method}</span>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "overlapping building type",
|
||||||
|
"render": "<div>The overlapping openstreetmap-building is a <b>{_osm_obj:building}</b> and covers <b>{_overlap_percentage}%</b> of the GRB building<div><h3>GRB geometry:</h3>{minimap(21, id):height:10rem;border-radius:1rem;overflow:hidden}<h3>OSM geometry:</h3>{minimap(21,_osm_obj:id):height:10rem;border-radius:1rem;overflow:hidden}",
|
||||||
|
"condition": "_overlaps_with!=null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "Import-button",
|
||||||
|
"render": "{import_button(building=$building; source:geometry:date=$_grb_date; source:geometry:ref=$_grb_ref, Upload this building to OpenStreetMap)}",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "_overlaps_with!=null",
|
||||||
|
"then": "Cannot be imported directly, there is a nearly identical building geometry in OpenStreetMap"
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
"all_tags"
|
"all_tags"
|
||||||
],
|
],
|
||||||
|
|
|
@ -108,7 +108,8 @@
|
||||||
"openLayerControl": "Open the layer control box",
|
"openLayerControl": "Open the layer control box",
|
||||||
"layerNotEnabled": "The layer {layer} is not enabled. Enable this layer to add a point",
|
"layerNotEnabled": "The layer {layer} is not enabled. Enable this layer to add a point",
|
||||||
"hasBeenImported": "This point has already been imported",
|
"hasBeenImported": "This point has already been imported",
|
||||||
"zoomInMore": "Zoom in more to import this feature"
|
"zoomInMore": "Zoom in more to import this feature",
|
||||||
|
"wrongType": "This element is not a point or a way and can not be imported"
|
||||||
},
|
},
|
||||||
"pickLanguage": "Choose a language: ",
|
"pickLanguage": "Choose a language: ",
|
||||||
"about": "Easily edit and add OpenStreetMap for a certain theme",
|
"about": "Easily edit and add OpenStreetMap for a certain theme",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue