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. " + | ||||
|                 "If the current feature is a point, all features that embed the point are given. " + | ||||
|                 "The returned value is `{ feat: GeoJSONFeature, overlap: number}[]` where `overlap` is the overlapping surface are (in m²) for areas, the overlapping length (in meter) if the current feature is a line or `undefined` if the current feature is a point.\n" + | ||||
|                 "The resulting list is sorted in descending order by overlap. The feature with the most overlap will thus be the first in the list" + | ||||
|                 "\n" + | ||||
|                 "For example to get all objects which overlap or embed from a layer, use `_contained_climbing_routes_properties=feat.overlapWith('climbing_route')`", | ||||
|             args: ["...layerIds - one or more layer ids  of the layer from which every feature is checked for overlap)"] | ||||
|         }, | ||||
|         (params, feat) => { | ||||
|             return (...layerIds: string[]) => { | ||||
|                 const result = [] | ||||
|                 const result : {feat:any, overlap: number}[]= [] | ||||
| 
 | ||||
|                 const bbox = BBox.get(feat) | ||||
| 
 | ||||
|  | @ -79,6 +80,9 @@ export class ExtraFunction { | |||
|                         result.push(...GeoOperations.calculateOverlap(feat, otherLayer)); | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 result.sort((a, b) => b.overlap - a.overlap) | ||||
|                  | ||||
|                 return result; | ||||
|             } | ||||
|         } | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ export interface ChangeDescription { | |||
|         /** | ||||
|          * The type of the change | ||||
|          */ | ||||
|         changeType:  "answer" | "create" | "split" | "delete" | "move" | string | ||||
|         changeType:  "answer" | "create" | "split" | "delete" | "move" | "import" | string | null | ||||
|         /** | ||||
|          * THe motivation for the change, e.g. 'deleted because does not exist anymore' | ||||
|          */ | ||||
|  | @ -51,7 +51,8 @@ export interface ChangeDescription { | |||
|         lat: number, | ||||
|         lon: number | ||||
|     } | { | ||||
|         // Coordinates are only used for rendering. They should be LAT, LON
 | ||||
|         /* Coordinates are only used for rendering. They should be LON, LAT | ||||
|         * */ | ||||
|         coordinates: [number, number][] | ||||
|         nodes: number[], | ||||
|     } | { | ||||
|  |  | |||
|  | @ -8,20 +8,29 @@ import {GeoOperations} from "../../GeoOperations"; | |||
| 
 | ||||
| export default class CreateNewNodeAction extends OsmChangeAction { | ||||
| 
 | ||||
|     /** | ||||
|      * Maps previously created points onto their assigned ID, to reuse the point if uplaoded | ||||
|      * "lat,lon" --> id | ||||
|      */ | ||||
|     private static readonly previouslyCreatedPoints = new Map<string, number>() | ||||
|     public newElementId: string = undefined | ||||
|     public newElementIdNumber: number = undefined | ||||
|     private readonly _basicTags: Tag[]; | ||||
|     private readonly _lat: number; | ||||
|     private readonly _lon: number; | ||||
|     private readonly _snapOnto: OsmWay; | ||||
|     private readonly _reusePointDistance: number; | ||||
|     private meta: { changeType: "create" | "import"; theme: string }; | ||||
|     private readonly _reusePreviouslyCreatedPoint: boolean; | ||||
| 
 | ||||
|     constructor(basicTags: Tag[], | ||||
|                 lat: number, lon: number, | ||||
|                 options: { | ||||
|                     snapOnto?: OsmWay,  | ||||
|                     reusePointWithinMeters?: number,  | ||||
|                     theme: string, changeType: "create" | "import" }) { | ||||
|                     allowReuseOfPreviouslyCreatedPoints?: boolean, | ||||
|                     snapOnto?: OsmWay, | ||||
|                     reusePointWithinMeters?: number, | ||||
|                     theme: string, changeType: "create" | "import" | null | ||||
|                 }) { | ||||
|         super() | ||||
|         this._basicTags = basicTags; | ||||
|         this._lat = lat; | ||||
|  | @ -31,18 +40,47 @@ export default class CreateNewNodeAction extends OsmChangeAction { | |||
|         } | ||||
|         this._snapOnto = options?.snapOnto; | ||||
|         this._reusePointDistance = options?.reusePointWithinMeters ?? 1 | ||||
|         this._reusePreviouslyCreatedPoint = options?.allowReuseOfPreviouslyCreatedPoints ?? (basicTags.length === 0) | ||||
|         this.meta = { | ||||
|             theme: options.theme, | ||||
|             changeType: options.changeType | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static registerIdRewrites(mappings: Map<string, string>) { | ||||
|         const toAdd: [string, number][] = [] | ||||
| 
 | ||||
|         this.previouslyCreatedPoints.forEach((oldId, key) => { | ||||
|             if (!mappings.has("node/" + oldId)) { | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             const newId = Number(mappings.get("node/" + oldId).substr("node/".length)) | ||||
|             toAdd.push([key, newId]) | ||||
|         }) | ||||
|         for (const [key, newId] of toAdd) { | ||||
|             CreateNewNodeAction.previouslyCreatedPoints.set(key, newId) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     async CreateChangeDescriptions(changes: Changes): Promise<ChangeDescription[]> { | ||||
|         if (this._reusePreviouslyCreatedPoint) { | ||||
| 
 | ||||
|             const key = this._lat + "," + this._lon | ||||
|             const prev = CreateNewNodeAction.previouslyCreatedPoints | ||||
|             if (prev.has(key)) { | ||||
|                 this.newElementIdNumber = prev.get(key) | ||||
|                 this.newElementId = "node/" + this.newElementIdNumber | ||||
|                 return [] | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const id = changes.getNewID() | ||||
|         const properties = { | ||||
|             id: "node/" + id | ||||
|         } | ||||
|         this.newElementId = "node/" + id | ||||
|         this.setElementId(id) | ||||
|         for (const kv of this._basicTags) { | ||||
|             if (typeof kv.value !== "string") { | ||||
|                 throw "Invalid value: don't use a regex in a preset" | ||||
|  | @ -84,8 +122,7 @@ export default class CreateNewNodeAction extends OsmChangeAction { | |||
|         } | ||||
|         if (reusedPointId !== undefined) { | ||||
|             console.log("Reusing an existing point:", reusedPointId) | ||||
|             this.newElementId = "node/" + reusedPointId | ||||
| 
 | ||||
|             this.setElementId(reusedPointId) | ||||
|             return [{ | ||||
|                 tags: new And(this._basicTags).asChange(properties), | ||||
|                 type: "node", | ||||
|  | @ -112,10 +149,20 @@ export default class CreateNewNodeAction extends OsmChangeAction { | |||
|                     coordinates: locations, | ||||
|                     nodes: ids | ||||
|                 }, | ||||
|                 meta:this.meta | ||||
|                 meta: this.meta | ||||
|             } | ||||
|         ] | ||||
|     } | ||||
| 
 | ||||
|     private setElementId(id: number) { | ||||
|         this.newElementIdNumber = id; | ||||
|         this.newElementId = "node/"+id | ||||
|         if (!this._reusePreviouslyCreatedPoint) { | ||||
|             return | ||||
|         } | ||||
|         const key = this._lat + "," + this._lon | ||||
|         CreateNewNodeAction.previouslyCreatedPoints.set(key, id) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										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 {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import SimpleMetaTagger from "../SimpleMetaTagger"; | ||||
| import CreateNewNodeAction from "./Actions/CreateNewNodeAction"; | ||||
| 
 | ||||
| /** | ||||
|  * Handles all changes made to OSM. | ||||
|  | @ -14,22 +15,20 @@ import SimpleMetaTagger from "../SimpleMetaTagger"; | |||
|  */ | ||||
| export class Changes { | ||||
| 
 | ||||
| 
 | ||||
|     private _nextId: number = -1; // Newly assigned ID's are negative
 | ||||
|     public readonly name = "Newly added features" | ||||
|     /** | ||||
|      * All the newly created features as featureSource + all the modified features | ||||
|      */ | ||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
| 
 | ||||
|     public readonly pendingChanges: UIEventSource<ChangeDescription[]> = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||
|     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) | ||||
|     private _nextId: number = -1; // Newly assigned ID's are negative
 | ||||
|     private readonly isUploading = new UIEventSource(false); | ||||
| 
 | ||||
|     private readonly previouslyCreated: OsmObject[] = [] | ||||
|     private readonly _leftRightSensitive: boolean; | ||||
| 
 | ||||
|     constructor(leftRightSensitive : boolean = false) { | ||||
|     constructor(leftRightSensitive: boolean = false) { | ||||
|         this._leftRightSensitive = leftRightSensitive; | ||||
|         // We keep track of all changes just as well
 | ||||
|         this.allChanges.setData([...this.pendingChanges.data]) | ||||
|  | @ -114,21 +113,34 @@ export class Changes { | |||
|             }) | ||||
|     } | ||||
| 
 | ||||
|     public async applyAction(action: OsmChangeAction): Promise<void> { | ||||
|         const changes = await action.Perform(this) | ||||
|         console.log("Received changes:", changes) | ||||
|         this.pendingChanges.data.push(...changes); | ||||
|         this.pendingChanges.ping(); | ||||
|         this.allChanges.data.push(...changes) | ||||
|         this.allChanges.ping() | ||||
|     } | ||||
| 
 | ||||
|     public registerIdRewrites(mappings: Map<string, string>): void { | ||||
|         CreateNewNodeAction.registerIdRewrites(mappings) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * UPload the selected changes to OSM. | ||||
|      * Returns 'true' if successfull and if they can be removed | ||||
|      * @param pending | ||||
|      * @private | ||||
|      */ | ||||
|     private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean>{ | ||||
|     private async flushSelectChanges(pending: ChangeDescription[]): Promise<boolean> { | ||||
|         const self = this; | ||||
|         const neededIds = Changes.GetNeededIds(pending) | ||||
|         const osmObjects = await Promise.all(neededIds.map(id => OsmObject.DownloadObjectAsync(id))); | ||||
|          | ||||
|         if(this._leftRightSensitive){ | ||||
| 
 | ||||
|         if (this._leftRightSensitive) { | ||||
|             osmObjects.forEach(obj => SimpleMetaTagger.removeBothTagging(obj.tags)) | ||||
|         } | ||||
|          | ||||
| 
 | ||||
|         console.log("Got the fresh objects!", osmObjects, "pending: ", pending) | ||||
|         const changes: { | ||||
|             newObjects: OsmObject[], | ||||
|  | @ -137,35 +149,38 @@ export class Changes { | |||
|         } = self.CreateChangesetObjects(pending, osmObjects) | ||||
|         if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { | ||||
|             console.log("No changes to be made") | ||||
|            return true | ||||
|             return true | ||||
|         } | ||||
| 
 | ||||
|         const meta = pending[0].meta | ||||
|          | ||||
|         const perType = Array.from(Utils.Hist(pending.map(descr => descr.meta.changeType)), ([key, count]) => ({ | ||||
|             key: key, | ||||
|                 value: count,  | ||||
|                 aggregate: true | ||||
|         })) | ||||
| 
 | ||||
|         const perType = Array.from( | ||||
|             Utils.Hist(pending.filter(descr => descr.meta.changeType !== undefined && descr.meta.changeType !== null) | ||||
|                 .map(descr => descr.meta.changeType)), ([key, count]) => ( | ||||
|                 { | ||||
|                     key: key, | ||||
|                     value: count, | ||||
|                     aggregate: true | ||||
|                 })) | ||||
|         const motivations = pending.filter(descr => descr.meta.specialMotivation !== undefined) | ||||
|             .map(descr => ({ | ||||
|                 key: descr.meta.changeType+":"+descr.type+"/"+descr.id, | ||||
|                     value: descr.meta.specialMotivation | ||||
|                 key: descr.meta.changeType + ":" + descr.type + "/" + descr.id, | ||||
|                 value: descr.meta.specialMotivation | ||||
|             })) | ||||
|         const metatags = [{ | ||||
|             key: "comment", | ||||
|             value: "Adding data with #MapComplete for theme #"+meta.theme | ||||
|             value: "Adding data with #MapComplete for theme #" + meta.theme | ||||
|         }, | ||||
|             { | ||||
|                 key:"theme", | ||||
|                 value:meta.theme | ||||
|                 key: "theme", | ||||
|                 value: meta.theme | ||||
|             }, | ||||
|             ...perType, | ||||
|             ...motivations | ||||
|         ] | ||||
|          | ||||
| 
 | ||||
|         await State.state.osmConnection.changesetHandler.UploadChangeset( | ||||
|             (csId) => Changes.createChangesetFor(""+csId, changes), | ||||
|             (csId) => Changes.createChangesetFor("" + csId, changes), | ||||
|             metatags | ||||
|         ) | ||||
| 
 | ||||
|  | @ -178,27 +193,27 @@ export class Changes { | |||
|         try { | ||||
|             // At last, we build the changeset and upload
 | ||||
|             const pending = self.pendingChanges.data; | ||||
|              | ||||
| 
 | ||||
|             const pendingPerTheme = new Map<string, ChangeDescription[]>() | ||||
|             for (const changeDescription of pending) { | ||||
|                 const theme = changeDescription.meta.theme | ||||
|                 if(!pendingPerTheme.has(theme)){ | ||||
|                 if (!pendingPerTheme.has(theme)) { | ||||
|                     pendingPerTheme.set(theme, []) | ||||
|                 } | ||||
|                 pendingPerTheme.get(theme).push(changeDescription) | ||||
|             } | ||||
|              | ||||
|           const successes =  await Promise.all(Array.from(pendingPerTheme, ([key , value]) => value) | ||||
| 
 | ||||
|             const successes = await Promise.all(Array.from(pendingPerTheme, ([key, value]) => value) | ||||
|                 .map(async pendingChanges => { | ||||
|                     try{ | ||||
|                     try { | ||||
|                         return await self.flushSelectChanges(pendingChanges); | ||||
|                     }catch(e){ | ||||
|                         console.error("Could not upload some changes:",e) | ||||
|                     } catch (e) { | ||||
|                         console.error("Could not upload some changes:", e) | ||||
|                         return false | ||||
|                     } | ||||
|                 })) | ||||
|              | ||||
|             if(!successes.some(s => s == false)){ | ||||
| 
 | ||||
|             if (!successes.some(s => s == false)) { | ||||
|                 // All changes successfull, we clear the data!
 | ||||
|                 this.pendingChanges.setData([]); | ||||
|             } | ||||
|  | @ -206,22 +221,13 @@ export class Changes { | |||
|         } catch (e) { | ||||
|             console.error("Could not handle changes - probably an old, pending changeset in localstorage with an invalid format; erasing those", e) | ||||
|             self.pendingChanges.setData([]) | ||||
|         }finally { | ||||
|         } finally { | ||||
|             self.isUploading.setData(false) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public async applyAction(action: OsmChangeAction): Promise<void> { | ||||
|         const changes = await action.Perform(this) | ||||
|         console.log("Received changes:", changes) | ||||
|         this.pendingChanges.data.push(...changes); | ||||
|         this.pendingChanges.ping(); | ||||
|         this.allChanges.data.push(...changes) | ||||
|         this.allChanges.ping() | ||||
|     } | ||||
| 
 | ||||
|     private CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { | ||||
|         newObjects: OsmObject[], | ||||
|         modifiedObjects: OsmObject[] | ||||
|  | @ -373,8 +379,4 @@ export class Changes { | |||
| 
 | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     public registerIdRewrites(mappings: Map<string, string>): void { | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -4,20 +4,34 @@ import {UIEventSource} from "../../Logic/UIEventSource"; | |||
| import Combine from "../Base/Combine"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import State from "../../State"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import Toggle from "../Input/Toggle"; | ||||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| 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 { | ||||
|     constructor(imageUrl: string | BaseUIElement, message: string | BaseUIElement, | ||||
|     constructor(imageUrl: string | BaseUIElement,  | ||||
|                 message: string | BaseUIElement, | ||||
|                 originalTags: UIEventSource<any>, | ||||
|                 newTags: UIEventSource<Tag[]>,  | ||||
|                 lat: number, lon: number, | ||||
|                 newTags: UIEventSource<Tag[]>, | ||||
|                 feature: any, | ||||
|                 minZoom: number, | ||||
|                 state: {    | ||||
|                 state: { | ||||
|                     featureSwitchUserbadge: UIEventSource<boolean>; | ||||
|                     featurePipeline: FeaturePipeline; | ||||
|                     allElements: ElementStorage; | ||||
|                     selectedElement: UIEventSource<any>; | ||||
|                     layoutToUse: LayoutConfig, | ||||
|                     osmConnection: OsmConnection, | ||||
|                     changes: Changes, | ||||
|                     locationControl: UIEventSource<{ zoom: number }> | ||||
|                 }) { | ||||
|         const t = Translations.t.general.add; | ||||
|  | @ -32,7 +46,7 @@ export default class ImportButton extends Toggle { | |||
|                     const txt = parts.join(" & ") | ||||
|                     return t.presetInfo.Subs({tags: txt}).SetClass("subtle") | ||||
|                 })), 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) | ||||
| 
 | ||||
|  | @ -44,15 +58,12 @@ export default class ImportButton extends Toggle { | |||
|             } | ||||
|             originalTags.data["_imported"] = "yes" | ||||
|             originalTags.ping() // will set isImported as per its definition
 | ||||
|             const newElementAction = new CreateNewNodeAction(newTags.data, lat, lon, { | ||||
|                 theme: State.state.layoutToUse.id, | ||||
|                 changeType: "import" | ||||
|             }) | ||||
|             await State.state.changes.applyAction(newElementAction) | ||||
|             State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( | ||||
|             const newElementAction = ImportButton.createAddActionForFeature(newTags.data, feature, state.layoutToUse.id) | ||||
|             await state.changes.applyAction(newElementAction) | ||||
|             state.selectedElement.setData(state.allElements.ContainingFeatures.get( | ||||
|                 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 | ||||
|             )) | ||||
| 
 | ||||
|  | @ -60,25 +71,70 @@ export default class ImportButton extends Toggle { | |||
|         }) | ||||
| 
 | ||||
|         const withLoadingCheck = new Toggle(new Toggle( | ||||
|             new Loading(t.stillLoading.Clone()), | ||||
|             new Combine([button, appliedTags]).SetClass("flex flex-col"), | ||||
|             State.state.featurePipeline.runningQuery | ||||
|         ),t.zoomInFurther.Clone(), | ||||
|                 state.locationControl.map(l => l.zoom >= minZoom)     | ||||
|             ) | ||||
|                 new Loading(t.stillLoading.Clone()), | ||||
|                 new Combine([button, appliedTags]).SetClass("flex flex-col"), | ||||
|                 state.featurePipeline.runningQuery | ||||
|             ), t.zoomInFurther.Clone(), | ||||
|             state.locationControl.map(l => l.zoom >= minZoom) | ||||
|         ) | ||||
|         const importButton = new Toggle(t.hasBeenImported, withLoadingCheck, isImported) | ||||
| 
 | ||||
|         const pleaseLoginButton = | ||||
|             new Toggle(t.pleaseLogin.Clone() | ||||
|                     .onClick(() => State.state.osmConnection.AttemptLogin()) | ||||
|                     .onClick(() => state.osmConnection.AttemptLogin()) | ||||
|                     .SetClass("login-button-friendly"), | ||||
|                 undefined, | ||||
|                 State.state.featureSwitchUserbadge) | ||||
|              | ||||
|                 state.featureSwitchUserbadge) | ||||
| 
 | ||||
|         super(importButton, | ||||
|             pleaseLoginButton, | ||||
|             State.state.osmConnection.isLoggedIn | ||||
| 
 | ||||
|         super(new Toggle(importButton, | ||||
|                 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 Link from "./Base/Link"; | ||||
| import List from "./Base/List"; | ||||
| import {OsmConnection} from "../Logic/Osm/OsmConnection"; | ||||
| 
 | ||||
| export interface SpecialVisualization { | ||||
|     funcName: string, | ||||
|  | @ -480,7 +481,7 @@ export default class SpecialVisualizations { | |||
|                 args: [ | ||||
|                     { | ||||
|                         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", | ||||
|  | @ -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.
 | ||||
| 
 | ||||
| #### Importing a dataset into OpenStreetMap: requirements | ||||
| 
 | ||||
| If you want to import a dataset, make sure that: | ||||
| 
 | ||||
| 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: | ||||
| 
 | ||||
| 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. | ||||
| 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) | ||||
| 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) => { | ||||
|                     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"), | ||||
|                             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 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 newTags: Tag [] = [] | ||||
|                         for (const [key, value] of tgsSpec) { | ||||
|                             if (value.startsWith('$')) { | ||||
|                                 const origKey = value.substring(1) | ||||
|                                 newTags.push(new Tag(key, tags[origKey])) | ||||
|                             if (value.indexOf('$') >= 0) { | ||||
|                                  | ||||
|                                 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 { | ||||
|                                 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 feature = state.allElements.ContainingFeatures.get(id) | ||||
|                     if (feature.geometry.type !== "Point") { | ||||
|                         return new FixedUiElement("Error: can only import point objects").SetClass("alert") | ||||
|                     } | ||||
|                     const [lon, lat] = feature.geometry.coordinates; | ||||
|                     const minzoom = Number(args[3]) | ||||
|                     const message =  args[1] | ||||
|                     const image = args[2] | ||||
|                      | ||||
|                     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", | ||||
|       "minzoom": 16, | ||||
|       "calculatedTags": [ | ||||
|         "_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 && feat.properties._surface - f.overlap < 5)[0] ?? null", | ||||
|         "_osm_obj:source:ref=JSON.parse(feat.properties._overlaps_with)?.feat?.properties['source:geometry:ref']", | ||||
|         "_osm_obj:source:date=JSON.parse(feat.properties._overlaps_with)?.feat?.properties['source:geometry:date'].replace(/\\//g, '-')", | ||||
|         "_imported_osm_object_found= feat.properties['_osm_obj:source:ref'] == feat.properties['source:geometry:entity'] + '/' + feat.properties['source:geometry:oidn']", | ||||
|         "_overlaps_with=feat.overlapWith('OSM-buildings').filter(f => f.overlap > 1 &&  (feat.get('_surface') < 20 || f.overlap / feat.get('_surface')) > 0.9)[0] ?? null", | ||||
|         "_overlap_absolute=feat.get('_overlaps_with')?.overlap", | ||||
|         "_overlap_percentage=Math.round(100 * feat.get('_overlap_absolute') / feat.get('_surface')) ", | ||||
|         "_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,'-')", | ||||
|         "_imported_osm_still_fresh= feat.properties['_osm_obj:source:date'] == feat.properties._grb_date" | ||||
|       ], | ||||
|       "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" | ||||
|       ], | ||||
|  |  | |||
|  | @ -108,7 +108,8 @@ | |||
|             "openLayerControl": "Open the layer control box", | ||||
|             "layerNotEnabled": "The layer {layer} is not enabled. Enable this layer to add a point", | ||||
|             "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: ", | ||||
|         "about": "Easily edit and add OpenStreetMap for a certain theme", | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue