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