forked from MapComplete/MapComplete
		
	Further development of split-road feature; refactoring of change-handling
This commit is contained in:
		
							parent
							
								
									dc4cdda3b5
								
							
						
					
					
						commit
						96ecded0b9
					
				
					 37 changed files with 967 additions and 568 deletions
				
			
		|  | @ -49,6 +49,7 @@ export default class LayerConfig { | |||
|     wayHandling: number; | ||||
|     public readonly units: Unit[]; | ||||
|     public readonly deletion: DeleteConfig | null | ||||
|     public readonly allowSplit: boolean | ||||
| 
 | ||||
|     presets: { | ||||
|         title: Translation, | ||||
|  | @ -67,6 +68,7 @@ export default class LayerConfig { | |||
|         context = context + "." + json.id; | ||||
|         const self = this; | ||||
|         this.id = json.id; | ||||
|         this.allowSplit = json.allowSplit ?? false; | ||||
|         this.name = Translations.T(json.name, context + ".name"); | ||||
|          | ||||
|         if(json.description !== undefined){ | ||||
|  |  | |||
|  | @ -291,4 +291,9 @@ export interface LayerConfigJson { | |||
|      */ | ||||
|     deletion?: boolean | DeleteConfigJson | ||||
| 
 | ||||
|     /** | ||||
|      * IF set, a 'split this road' button is shown | ||||
|      */ | ||||
|     allowSplit?: boolean | ||||
| 
 | ||||
| } | ||||
|  | @ -65,7 +65,7 @@ export default class LayoutConfig { | |||
|             this.language = json.language; | ||||
|         } | ||||
|         if (this.language.length == 0) { | ||||
|             throw "No languages defined. Define at least one language" | ||||
|             throw `No languages defined. Define at least one language. (${context}.languages)` | ||||
|         } | ||||
|         if (json.title === undefined) { | ||||
|             throw "Title not defined in " + this.id; | ||||
|  |  | |||
|  | @ -420,10 +420,10 @@ export class InitUiElements { | |||
| 
 | ||||
| 
 | ||||
|         const source = new FeaturePipeline(state.filteredLayers, | ||||
|             State.state.changes, | ||||
|             updater, | ||||
|             state.osmApiFeatureSource, | ||||
|             state.layoutToUse, | ||||
|             state.changes, | ||||
|             state.locationControl, | ||||
|             state.selectedElement); | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										36
									
								
								Logic/Actors/ChangeToElementsActor.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								Logic/Actors/ChangeToElementsActor.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| 
 | ||||
| export default class ChangeToElementsActor { | ||||
|     constructor(changes: Changes, allElements: ElementStorage) { | ||||
|         changes.pendingChanges.addCallbackAndRun(changes => { | ||||
|             for (const change of changes) { | ||||
|                 const id = change.type + "/" + change.id; | ||||
|                 if (!allElements.has(id)) { | ||||
| continue; // Will be picked up later on
 | ||||
|                 } | ||||
|                 const src = allElements.getEventSourceById(id) | ||||
| 
 | ||||
|                 let changed = false; | ||||
|                 for (const kv of change.tags ?? []) { | ||||
|                     // Apply tag changes and ping the consumers
 | ||||
|                     const k = kv.k | ||||
|                     let v = kv.v | ||||
|                     if (v === "") { | ||||
|                         v = undefined; | ||||
|                     } | ||||
|                     if (src.data[k] === v) { | ||||
|                         continue | ||||
|                     } | ||||
|                     changed = true; | ||||
|                     src.data[k] = v; | ||||
|                 } | ||||
|                 if (changed) { | ||||
|                     src.ping() | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | @ -9,7 +9,7 @@ export default class PendingChangesUploader { | |||
|     constructor(changes: Changes, selectedFeature: UIEventSource<any>) { | ||||
|         const self = this; | ||||
|         this.lastChange = new Date(); | ||||
|         changes.pending.addCallback(() => { | ||||
|         changes.pendingChanges.addCallback(() => { | ||||
|             self.lastChange = new Date(); | ||||
| 
 | ||||
|             window.setTimeout(() => { | ||||
|  | @ -54,7 +54,7 @@ export default class PendingChangesUploader { | |||
| 
 | ||||
| 
 | ||||
|         function onunload(e) { | ||||
|             if (changes.pending.data.length == 0) { | ||||
|             if(changes.pendingChanges.data.length == 0){ | ||||
|                 return; | ||||
|             } | ||||
|             changes.flushChanges("onbeforeunload - probably closing or something similar"); | ||||
|  |  | |||
							
								
								
									
										138
									
								
								Logic/FeatureSource/ChangeApplicator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								Logic/FeatureSource/ChangeApplicator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,138 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import {ChangeDescription} from "../Osm/Actions/ChangeDescription"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {OsmNode, OsmRelation, OsmWay} from "../Osm/OsmObject"; | ||||
| 
 | ||||
| /** | ||||
|  * Applies changes from 'Changes' onto a featureSource | ||||
|  */ | ||||
| export default class ChangeApplicator implements FeatureSource { | ||||
|     public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||
|     public readonly name: string; | ||||
| 
 | ||||
|     constructor(source: FeatureSource, changes: Changes) { | ||||
| 
 | ||||
|         this.name = "ChangesApplied(" + source.name + ")" | ||||
|         this.features = source.features | ||||
| 
 | ||||
|         source.features.addCallbackAndRunD(features => { | ||||
|             ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data) | ||||
|         }) | ||||
| 
 | ||||
|         changes.pendingChanges.addCallbackAndRunD(changes => { | ||||
|             ChangeApplicator.ApplyChanges(source.features.data, changes) | ||||
|             source.features.ping() | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static ApplyChanges(features: { feature: any, freshness: Date }[], cs: ChangeDescription[]) { | ||||
|         if (cs.length === 0 || features === undefined) { | ||||
|             return features; | ||||
|         } | ||||
| 
 | ||||
|         const changesPerId: Map<string, ChangeDescription[]> = new Map<string, ChangeDescription[]>() | ||||
|         for (const c of cs) { | ||||
|             const id = c.type + "/" + c.id | ||||
|             if (!changesPerId.has(id)) { | ||||
|                 changesPerId.set(id, []) | ||||
|             } | ||||
|             changesPerId.get(id).push(c) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const now = new Date() | ||||
| 
 | ||||
|         function add(feature) { | ||||
|             features.push({ | ||||
|                 feature: feature, | ||||
|                 freshness: now | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         // First, create the new features - they have a negative ID
 | ||||
|         // We don't set the properties yet though
 | ||||
|         changesPerId.forEach(cs => { | ||||
|             cs.forEach(change => { | ||||
|                 if (change.id >= 0) { | ||||
|                     return; // Nothing to do here, already created
 | ||||
|                 } | ||||
| 
 | ||||
| 
 | ||||
|                 try { | ||||
| 
 | ||||
|                     switch (change.type) { | ||||
|                         case "node": | ||||
|                             const n = new OsmNode(change.id) | ||||
|                             n.lat = change.changes["lat"] | ||||
|                             n.lon = change.changes["lon"] | ||||
|                             const geojson = n.asGeoJson() | ||||
|                             add(geojson) | ||||
|                             break; | ||||
|                         case "way": | ||||
|                             const w = new OsmWay(change.id) | ||||
|                             w.nodes = change.changes["nodes"] | ||||
|                             add(w.asGeoJson()) | ||||
|                             break; | ||||
|                         case "relation": | ||||
|                             const r = new OsmRelation(change.id) | ||||
|                             r.members = change.changes["members"] | ||||
|                             add(r.asGeoJson()) | ||||
|                             break; | ||||
|                     } | ||||
| 
 | ||||
|                 } catch (e) { | ||||
|                     console.error(e) | ||||
|                 } | ||||
|             }) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         for (const feature of features) { | ||||
|             const id = feature.feature.properties.id; | ||||
|             const f = feature.feature; | ||||
|             if (!changesPerId.has(id)) { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             const changed = {} | ||||
|             // Copy all the properties
 | ||||
|             Utils.Merge(f, changed) | ||||
|             // play the changes onto the copied object
 | ||||
| 
 | ||||
|             for (const change of changesPerId.get(id)) { | ||||
|                 for (const kv of change.tags ?? []) { | ||||
|                     // Apply tag changes and ping the consumers
 | ||||
|                     const k = kv.k | ||||
|                     let v = kv.v | ||||
|                     if (v === "") { | ||||
|                         v = undefined; | ||||
|                     } | ||||
|                     f.properties[k] = v; | ||||
|                 } | ||||
| 
 | ||||
|                 // Apply other changes to the object
 | ||||
|                 if (change.changes !== undefined) { | ||||
|                     switch (change.type) { | ||||
|                         case "node": | ||||
|                             // @ts-ignore
 | ||||
|                             const coor: { lat, lon } = change.changes; | ||||
|                             f.geometry.coordinates = [[coor.lon, coor.lat]] | ||||
|                             break; | ||||
|                         case "way": | ||||
|                             f.geometry.coordinates = change.changes["locations"] | ||||
|                             break; | ||||
|                         case "relation": | ||||
|                             console.error("Changes to relations are not yet supported") | ||||
|                             break; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -13,6 +13,8 @@ import Loc from "../../Models/Loc"; | |||
| import GeoJsonSource from "./GeoJsonSource"; | ||||
| import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource"; | ||||
| import RegisteringFeatureSource from "./RegisteringFeatureSource"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import ChangeApplicator from "./ChangeApplicator"; | ||||
| 
 | ||||
| export default class FeaturePipeline implements FeatureSource { | ||||
| 
 | ||||
|  | @ -21,10 +23,10 @@ export default class FeaturePipeline implements FeatureSource { | |||
|     public readonly name = "FeaturePipeline" | ||||
| 
 | ||||
|     constructor(flayers: UIEventSource<{ isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>, | ||||
|                 changes: Changes, | ||||
|                 updater: FeatureSource, | ||||
|                 fromOsmApi: FeatureSource, | ||||
|                 layout: UIEventSource<LayoutConfig>, | ||||
|                 newPoints: FeatureSource, | ||||
|                 locationControl: UIEventSource<Loc>, | ||||
|                 selectedElement: UIEventSource<any>) { | ||||
| 
 | ||||
|  | @ -40,13 +42,16 @@ export default class FeaturePipeline implements FeatureSource { | |||
|                     new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|                         new FeatureDuplicatorPerLayer(flayers, | ||||
|                             new RegisteringFeatureSource( | ||||
|                                 updater) | ||||
|                                 new ChangeApplicator( | ||||
|                                     updater, changes | ||||
|                                 )) | ||||
|                         )), layout)); | ||||
| 
 | ||||
|         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||
|             .ConstructMultiSource(flayers.data, locationControl) | ||||
|             .map(geojsonSource => { | ||||
|                 let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource)); | ||||
|                 let source = new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers,  | ||||
|                     new ChangeApplicator(geojsonSource, changes))); | ||||
|                 if(!geojsonSource.isOsmCache){ | ||||
|                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); | ||||
|                 } | ||||
|  | @ -54,25 +59,19 @@ export default class FeaturePipeline implements FeatureSource { | |||
|             }); | ||||
| 
 | ||||
|         const amendedLocalStorageSource = | ||||
|             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)) | ||||
|             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers,new ChangeApplicator( new LocalStorageSource(layout), changes)) | ||||
|             )); | ||||
| 
 | ||||
|         newPoints = new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|             new FeatureDuplicatorPerLayer(flayers, | ||||
|                 new RegisteringFeatureSource(newPoints))); | ||||
| 
 | ||||
|         const amendedOsmApiSource = new RememberingSource( | ||||
|             new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|                 new FeatureDuplicatorPerLayer(flayers, | ||||
| 
 | ||||
|                     new RegisteringFeatureSource(fromOsmApi)))); | ||||
|                     new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes))))); | ||||
| 
 | ||||
|         const merged = | ||||
|             new FeatureSourceMerger([ | ||||
|                 amendedOverpassSource, | ||||
|                 amendedOsmApiSource, | ||||
|                 amendedLocalStorageSource, | ||||
|                 newPoints, | ||||
|                 ...geojsonSources | ||||
|             ]); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +0,0 @@ | |||
| /** | ||||
|  * An action is a change to the OSM-database | ||||
|  * It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object | ||||
|  */ | ||||
| export default interface Action { | ||||
|      | ||||
| } | ||||
							
								
								
									
										30
									
								
								Logic/Osm/Actions/ChangeDescription.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Logic/Osm/Actions/ChangeDescription.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,30 @@ | |||
| export interface ChangeDescription { | ||||
| 
 | ||||
|     type: "node" | "way" | "relation", | ||||
|     /** | ||||
|      * Negative for a new objects | ||||
|      */ | ||||
|     id: number, | ||||
|     /* | ||||
|  v = "" or v = undefined to erase this tag | ||||
|  */ | ||||
|     tags?: { k: string, v: string }[], | ||||
| 
 | ||||
|     changes?: { | ||||
|         lat: number, | ||||
|         lon: number | ||||
|     } | { | ||||
|         // Coordinates are only used for rendering
 | ||||
|         locations: [number, number][] | ||||
|         nodes: number[], | ||||
|     } | { | ||||
|         members: { type: "node" | "way" | "relation", ref: number, role: string }[] | ||||
|     } | ||||
| 
 | ||||
|     /* | ||||
|     Set to delete the object | ||||
|      */ | ||||
|     doDelete?: boolean | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										52
									
								
								Logic/Osm/Actions/ChangeTagAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								Logic/Osm/Actions/ChangeTagAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,52 @@ | |||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {TagsFilter} from "../../Tags/TagsFilter"; | ||||
| 
 | ||||
| export default class ChangeTagAction extends OsmChangeAction { | ||||
|     private readonly _elementId: string; | ||||
|     private readonly _tagsFilter: TagsFilter; | ||||
|     private readonly _currentTags: any; | ||||
| 
 | ||||
|     constructor(elementId: string, tagsFilter: TagsFilter, currentTags: any) { | ||||
|         super(); | ||||
|         this._elementId = elementId; | ||||
|         this._tagsFilter = tagsFilter; | ||||
|         this._currentTags = currentTags; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Doublechecks that no stupid values are added | ||||
|      */ | ||||
|     private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { | ||||
|         const key = kv.k; | ||||
|         const value = kv.v; | ||||
|         if (key === undefined || key === null) { | ||||
|             console.log("Invalid key"); | ||||
|             return undefined; | ||||
|         } | ||||
|         if (value === undefined || value === null) { | ||||
|             console.log("Invalid value for ", key); | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         if (key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")) { | ||||
|             console.warn("Tag starts with or ends with a space - trimming anyway") | ||||
|         } | ||||
| 
 | ||||
|         return {k: key.trim(), v: value.trim()}; | ||||
|     } | ||||
| 
 | ||||
|     Perform(changes: Changes): ChangeDescription [] { | ||||
|         const changedTags: { k: string, v: string }[] = this._tagsFilter.asChange(this._currentTags).map(ChangeTagAction.checkChange) | ||||
|         const typeId = this._elementId.split("/") | ||||
|         const type = typeId[0] | ||||
|         const id = Number(typeId  [1]) | ||||
|         return [{ | ||||
|             // @ts-ignore
 | ||||
|             type: type, | ||||
|             id: id, | ||||
|             tags: changedTags | ||||
|         }] | ||||
|     } | ||||
| } | ||||
							
								
								
									
										44
									
								
								Logic/Osm/Actions/CreateNewNodeAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								Logic/Osm/Actions/CreateNewNodeAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,44 @@ | |||
| import {Tag} from "../../Tags/Tag"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {And} from "../../Tags/And"; | ||||
| 
 | ||||
| export default class CreateNewNodeAction implements OsmChangeAction { | ||||
| 
 | ||||
|     private readonly _basicTags: Tag[]; | ||||
|     private readonly _lat: number; | ||||
|     private readonly _lon: number; | ||||
| 
 | ||||
|     constructor(basicTags: Tag[], lat: number, lon: number) { | ||||
|         this._basicTags = basicTags; | ||||
|         this._lat = lat; | ||||
|         this._lon = lon; | ||||
|     } | ||||
| 
 | ||||
|     Perform(changes: Changes): ChangeDescription[] { | ||||
|         const id = changes.getNewID() | ||||
|         const properties = { | ||||
|             id: "node/" + id | ||||
|         } | ||||
|         for (const kv of this._basicTags) { | ||||
|             if (typeof kv.value !== "string") { | ||||
|                 throw "Invalid value: don't use a regex in a preset" | ||||
|             } | ||||
|             properties[kv.key] = kv.value; | ||||
|         } | ||||
| 
 | ||||
|         return [{ | ||||
|             tags: new And(this._basicTags).asChange(properties), | ||||
|             type: "node", | ||||
|             id: id, | ||||
|             changes:{ | ||||
|                 lat: this._lat, | ||||
|                 lon: this._lon | ||||
|             } | ||||
|         }] | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,9 +1,9 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Translation} from "../../UI/i18n/Translation"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {OsmObject} from "./OsmObject"; | ||||
| import State from "../../State"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Translation} from "../../../UI/i18n/Translation"; | ||||
| import State from "../../../State"; | ||||
| import {OsmObject} from "../OsmObject"; | ||||
| import Translations from "../../../UI/i18n/Translations"; | ||||
| import Constants from "../../../Models/Constants"; | ||||
| 
 | ||||
| export default class DeleteAction { | ||||
| 
 | ||||
|  | @ -30,7 +30,7 @@ export default class DeleteAction { | |||
|      * Does actually delete the feature; returns the event source 'this.isDeleted' | ||||
|      * If deletion is not allowed, triggers the callback instead | ||||
|      */ | ||||
|     public DoDelete(reason: string, onNotAllowed : () => void): UIEventSource<boolean> { | ||||
|     public DoDelete(reason: string, onNotAllowed : () => void): void { | ||||
|         const isDeleted = this.isDeleted | ||||
|         const self = this; | ||||
|         let deletionStarted = false; | ||||
|  | @ -75,8 +75,6 @@ export default class DeleteAction { | |||
| 
 | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         return isDeleted; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
							
								
								
									
										16
									
								
								Logic/Osm/Actions/OsmChangeAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								Logic/Osm/Actions/OsmChangeAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,16 @@ | |||
| /** | ||||
|  * An action is a change to the OSM-database | ||||
|  * It will generate some new/modified/deleted objects, which are all bundled by the 'changes'-object | ||||
|  */ | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| 
 | ||||
| export default abstract class OsmChangeAction { | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     public abstract Perform(changes: Changes): ChangeDescription[] | ||||
|      | ||||
|      | ||||
|      | ||||
| } | ||||
							
								
								
									
										20
									
								
								Logic/Osm/Actions/RelationSplitlHandler.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								Logic/Osm/Actions/RelationSplitlHandler.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| /** | ||||
|  * The logic to handle relations after a way within  | ||||
|  */ | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import {OsmRelation, OsmWay} from "../OsmObject"; | ||||
| 
 | ||||
| export default class RelationSplitlHandler extends OsmChangeAction{ | ||||
| 
 | ||||
|     constructor(partOf: OsmRelation[], newWayIds: number[], originalNodes: number[]) { | ||||
|         super() | ||||
|     } | ||||
| 
 | ||||
|     Perform(changes: Changes): ChangeDescription[] { | ||||
|         return []; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
							
								
								
									
										236
									
								
								Logic/Osm/Actions/SplitAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										236
									
								
								Logic/Osm/Actions/SplitAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,236 @@ | |||
| import {OsmRelation, OsmWay} from "../OsmObject"; | ||||
| import {Changes} from "../Changes"; | ||||
| import {GeoOperations} from "../../GeoOperations"; | ||||
| import OsmChangeAction from "./OsmChangeAction"; | ||||
| import {ChangeDescription} from "./ChangeDescription"; | ||||
| import RelationSplitlHandler from "./RelationSplitlHandler"; | ||||
| 
 | ||||
| interface SplitInfo { | ||||
|     originalIndex?: number, // or negative for new elements
 | ||||
|     lngLat: [number, number], | ||||
|     doSplit: boolean | ||||
| } | ||||
| 
 | ||||
| export default class SplitAction extends OsmChangeAction { | ||||
|     private readonly roadObject: any; | ||||
|     private readonly osmWay: OsmWay; | ||||
|     private _partOf: OsmRelation[]; | ||||
|     private readonly _splitPoints: any[]; | ||||
| 
 | ||||
|     constructor(osmWay: OsmWay, wayGeoJson: any, partOf: OsmRelation[], splitPoints: any[]) { | ||||
|         super() | ||||
|         this.osmWay = osmWay; | ||||
|         this.roadObject = wayGeoJson; | ||||
|         this._partOf = partOf; | ||||
|         this._splitPoints = splitPoints; | ||||
|     } | ||||
| 
 | ||||
|     private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { | ||||
|         const wayParts = [] | ||||
|         let currentPart = [] | ||||
|         for (const splitInfoElement of splitInfo) { | ||||
|             currentPart.push(splitInfoElement) | ||||
| 
 | ||||
|             if (splitInfoElement.doSplit) { | ||||
|                 // We have to do a split!
 | ||||
|                 // We add the current index to the currentParts, flush it and add it again
 | ||||
|                 wayParts.push(currentPart) | ||||
|                 currentPart = [splitInfoElement] | ||||
|             } | ||||
|         } | ||||
|         wayParts.push(currentPart) | ||||
|         return wayParts.filter(wp => wp.length > 0) | ||||
|     } | ||||
| 
 | ||||
|     Perform(changes: Changes): ChangeDescription[] { | ||||
|         const splitPoints = this._splitPoints | ||||
|         // We mark the new split points with a new id
 | ||||
|         console.log(splitPoints) | ||||
|         for (const splitPoint of splitPoints) { | ||||
|             splitPoint.properties["_is_split_point"] = true | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const self = this; | ||||
|         const partOf = this._partOf | ||||
|         const originalElement = this.osmWay | ||||
|         const originalNodes = this.osmWay.nodes; | ||||
| 
 | ||||
|         // First, calculate splitpoints and remove points close to one another
 | ||||
|         const splitInfo = self.CalculateSplitCoordinates(splitPoints) | ||||
|         // Now we have a list with e.g. 
 | ||||
|         // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 | ||||
| 
 | ||||
|         // Lets change 'originalIndex' to the actual node id first:
 | ||||
|         for (const element of splitInfo) { | ||||
|             if (element.originalIndex >= 0) { | ||||
|                 element.originalIndex = originalElement.nodes[element.originalIndex] | ||||
|             } else { | ||||
|                 element.originalIndex = changes.getNewID(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Next up is creating actual parts from this
 | ||||
|         const wayParts: SplitInfo[][] = SplitAction.SegmentSplitInfo(splitInfo); | ||||
| console.log("WayParts", wayParts, "by", splitInfo) | ||||
|         // Allright! At this point, we have our new ways!
 | ||||
|         // Which one is the longest of them (and can keep the id)?
 | ||||
| 
 | ||||
|         let longest = undefined; | ||||
|         for (const wayPart of wayParts) { | ||||
|             if (longest === undefined) { | ||||
|                 longest = wayPart; | ||||
|                 continue | ||||
|             } | ||||
|             if (wayPart.length > longest.length) { | ||||
|                 longest = wayPart | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         const changeDescription: ChangeDescription[] = [] | ||||
|         // Let's create the new points as needed
 | ||||
|         for (const element of splitInfo) { | ||||
|             if (element.originalIndex >= 0) { | ||||
|                 continue; | ||||
|             } | ||||
|             changeDescription.push({ | ||||
|                 type: "node", | ||||
|                 id: element.originalIndex, | ||||
|                 changes:{ | ||||
|                     lon:  element.lngLat[0], | ||||
|                     lat: element.lngLat[1] | ||||
|                 } | ||||
|             }) | ||||
|         } | ||||
| 
 | ||||
|         const newWayIds: number[] = [] | ||||
|         // Lets create OsmWays based on them
 | ||||
|         for (const wayPart of wayParts) { | ||||
| 
 | ||||
|             let isOriginal = wayPart === longest | ||||
|             if (isOriginal) { | ||||
|                 // We change the actual element!
 | ||||
|                 changeDescription.push({ | ||||
|                     type:"way", | ||||
|                     id: originalElement.id, | ||||
|                     changes:{ | ||||
|                         locations: wayPart.map(p => p.lngLat), | ||||
|                         nodes:  wayPart.map(p => p.originalIndex) | ||||
|                     } | ||||
|                 }) | ||||
|             } else { | ||||
|                 let id = changes.getNewID(); | ||||
|                 newWayIds.push(id) | ||||
|                  | ||||
|                 const kv = [] | ||||
|                 for (const k in originalElement.tags) { | ||||
|                     if(!originalElement.tags.hasOwnProperty(k)){ | ||||
|                         continue | ||||
|                     } | ||||
|                     kv .push({k: k, v: originalElement.tags[k]}) | ||||
|                 } | ||||
|                 changeDescription.push({ | ||||
|                     type:"way", | ||||
|                     id:id, | ||||
|                     tags: kv, | ||||
|                     changes:{ | ||||
|                         locations: wayPart.map(p => p.lngLat), | ||||
|                         nodes:  wayPart.map(p => p.originalIndex) | ||||
|                     } | ||||
|                 }) | ||||
|             } | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         // At last, we still have to check that we aren't part of a relation...
 | ||||
|         // At least, the order of the ways is identical, so we can keep the same roles
 | ||||
|         changeDescription.push(...new RelationSplitlHandler(partOf, newWayIds, originalNodes).Perform(changes)) | ||||
| 
 | ||||
|         // And we have our objects!
 | ||||
|         // Time to upload
 | ||||
| 
 | ||||
|       return changeDescription | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Calculates the actual points to split | ||||
|      * If another point is closer then ~5m, we reuse that point | ||||
|      */ | ||||
|     private CalculateSplitCoordinates( | ||||
|         splitPoints: any[], | ||||
|         toleranceInM = 5): SplitInfo[] { | ||||
| 
 | ||||
|         const allPoints = [...splitPoints]; | ||||
|         // We have a bunch of coordinates here: [ [lat, lon], [lat, lon], ...] ...
 | ||||
|         const originalPoints: [number, number][] = this.roadObject.geometry.coordinates | ||||
|         // We project them onto the line (which should yield pretty much the same point
 | ||||
|         for (let i = 0; i < originalPoints.length; i++) { | ||||
|             let originalPoint = originalPoints[i]; | ||||
|             let projected = GeoOperations.nearestPoint(this.roadObject, originalPoint) | ||||
|             projected.properties["_is_split_point"] = false | ||||
|             projected.properties["_original_index"] = i | ||||
|             allPoints.push(projected) | ||||
|         } | ||||
|         // At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
 | ||||
|         // We sort this list so that the new points are at the same location
 | ||||
|         allPoints.sort((a, b) => a.properties.location - b.properties.location) | ||||
| 
 | ||||
|         // When this is done, we check that no now point is too close to an already existing point and no very small segments get created
 | ||||
| 
 | ||||
|      /*   for (let i = allPoints.length - 1; i > 0; i--) { | ||||
| 
 | ||||
|             const point = allPoints[i]; | ||||
|             if (point.properties._original_index !== undefined) { | ||||
|                 // This point is already in OSM - we have to keep it!
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (i != allPoints.length - 1) { | ||||
|                 const prevPoint = allPoints[i + 1] | ||||
|                 const diff = Math.abs(point.properties.location - prevPoint.properties.location) * 1000 | ||||
|                 if (diff <= toleranceInM) { | ||||
|                     // To close to the previous point! We delete this point...
 | ||||
|                     allPoints.splice(i, 1) | ||||
|                     // ... and mark the previous point as a split point
 | ||||
|                     prevPoint.properties._is_split_point = true | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (i > 0) { | ||||
|                 const nextPoint = allPoints[i - 1] | ||||
|                 const diff = Math.abs(point.properties.location - nextPoint.properties.location) * 1000 | ||||
|                 if (diff <= toleranceInM) { | ||||
|                     // To close to the next point! We delete this point...
 | ||||
|                     allPoints.splice(i, 1) | ||||
|                     // ... and mark the next point as a split point
 | ||||
|                     nextPoint.properties._is_split_point = true | ||||
|                     // noinspection UnnecessaryContinueJS
 | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|             // We don't have to remove this point...
 | ||||
|         }*/ | ||||
| 
 | ||||
|         const splitInfo: SplitInfo[] = [] | ||||
|         let nextId = -1 | ||||
| 
 | ||||
|         for (const p of allPoints) { | ||||
|             let index = p.properties._original_index | ||||
|             if (index === undefined) { | ||||
|                 index = nextId; | ||||
|                 nextId--; | ||||
|             } | ||||
|             const splitInfoElement = { | ||||
|                 originalIndex: index, | ||||
|                 lngLat: p.geometry.coordinates, | ||||
|                 doSplit: p.properties._is_split_point | ||||
|             } | ||||
|             splitInfo.push(splitInfoElement) | ||||
|         } | ||||
| 
 | ||||
|         return splitInfo | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,81 +1,225 @@ | |||
| import {OsmNode, OsmObject} from "./OsmObject"; | ||||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | ||||
| import State from "../../State"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import {Tag} from "../Tags/Tag"; | ||||
| import {OsmConnection} from "./OsmConnection"; | ||||
| import OsmChangeAction from "./Actions/OsmChangeAction"; | ||||
| import {ChangeDescription} from "./Actions/ChangeDescription"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| import {Utils} from "../../Utils"; | ||||
| 
 | ||||
| /** | ||||
|  * Handles all changes made to OSM. | ||||
|  * Needs an authenticator via OsmConnection | ||||
|  */ | ||||
| export class Changes implements FeatureSource { | ||||
| export class Changes { | ||||
| 
 | ||||
| 
 | ||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||
|     public readonly name = "Newly added features" | ||||
|     /** | ||||
|      * The newly created points, as a FeatureSource | ||||
|      * All the newly created features as featureSource + all the modified features | ||||
|      */ | ||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
|     /** | ||||
|      * All the pending changes | ||||
|      */ | ||||
|     public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) | ||||
| 
 | ||||
|     /** | ||||
|      * All the pending new objects to upload | ||||
|      */ | ||||
|     private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) | ||||
| 
 | ||||
|     public readonly pendingChanges = new UIEventSource<ChangeDescription[]>([]) //  LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", [])
 | ||||
|     private readonly isUploading = new UIEventSource(false); | ||||
| 
 | ||||
|     /** | ||||
|      * Adds a change to the pending changes | ||||
|      */ | ||||
|     private static checkChange(kv: { k: string, v: string }): { k: string, v: string } { | ||||
|         const key = kv.k; | ||||
|         const value = kv.v; | ||||
|         if (key === undefined || key === null) { | ||||
|             console.log("Invalid key"); | ||||
|             return undefined; | ||||
|         } | ||||
|         if (value === undefined || value === null) { | ||||
|             console.log("Invalid value for ", key); | ||||
|             return undefined; | ||||
|         } | ||||
| 
 | ||||
|         if (key.startsWith(" ") || value.startsWith(" ") || value.endsWith(" ") || key.endsWith(" ")) { | ||||
|             console.warn("Tag starts with or ends with a space - trimming anyway") | ||||
|         } | ||||
| 
 | ||||
|         return {k: key.trim(), v: value.trim()}; | ||||
|     constructor() { | ||||
|         this.isUploading.addCallbackAndRun(u => { | ||||
|             if (u) { | ||||
|                 console.trace("Uploading set!") | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
| 
 | ||||
|     public static createChangesetFor(csId: string, | ||||
|                                      allChanges: { | ||||
|                                          modifiedObjects?: OsmObject[], | ||||
|                                          newElements?: OsmObject[], | ||||
|                                          deletedElements?: OsmObject[] | ||||
|                                      }): string { | ||||
| 
 | ||||
|     addTag(elementId: string, tagsFilter: TagsFilter, | ||||
|            tags?: UIEventSource<any>) { | ||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||
|         const elementTags = eventSource.data; | ||||
|         const changes = tagsFilter.asChange(elementTags).map(Changes.checkChange) | ||||
|         if (changes.length == 0) { | ||||
|             return; | ||||
|         const changedElements = allChanges.modifiedObjects ?? [] | ||||
|         const newElements = allChanges.newElements ?? [] | ||||
|         const deletedElements = allChanges.deletedElements ?? [] | ||||
| 
 | ||||
|         let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`; | ||||
|         if (newElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<create>\n" + | ||||
|                 newElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "</create>"; | ||||
|         } | ||||
|         if (changedElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<modify>\n" + | ||||
|                 changedElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</modify>"; | ||||
|         } | ||||
| 
 | ||||
|         if (deletedElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<deleted>\n" + | ||||
|                 deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</deleted>" | ||||
|         } | ||||
| 
 | ||||
|         changes += "</osmChange>"; | ||||
|         return changes; | ||||
|     } | ||||
| 
 | ||||
|     private static GetNeededIds(changes: ChangeDescription[]) { | ||||
|         return Utils.Dedup(changes.filter(c => c.id >= 0) | ||||
|             .map(c => c.type + "/" + c.id)) | ||||
|     } | ||||
| 
 | ||||
|     private static CreateChangesetObjects(changes: ChangeDescription[], downloadedOsmObjects: OsmObject[]): { | ||||
|         newObjects: OsmObject[], | ||||
|         modifiedObjects: OsmObject[] | ||||
|         deletedObjects: OsmObject[] | ||||
| 
 | ||||
|     } { | ||||
|         const objects: Map<string, OsmObject> = new Map<string, OsmObject>() | ||||
|         const states: Map<string, "unchanged" | "created" | "modified" | "deleted"> = new Map(); | ||||
| 
 | ||||
|         for (const o of downloadedOsmObjects) { | ||||
|             objects.set(o.type + "/" + o.id, o) | ||||
|             states.set(o.type + "/" + o.id, "unchanged") | ||||
|         } | ||||
| 
 | ||||
|         let changed = false; | ||||
|         for (const change of changes) { | ||||
|             if (elementTags[change.k] !== change.v) { | ||||
|                 elementTags[change.k] = change.v; | ||||
|                 console.log("Applied ", change.k, "=", change.v) | ||||
|                 // We use 'elementTags.id' here, as we might have retrieved with the id 'node/-1' as new point, but should use the rewritten id
 | ||||
|                 this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v}); | ||||
|             const id = change.type + "/" + change.id | ||||
|             if (!objects.has(id)) { | ||||
|                 // This is a new object that should be created
 | ||||
|                 states.set(id, "created") | ||||
|                 let osmObj: OsmObject = undefined; | ||||
|                 switch (change.type) { | ||||
|                     case "node": | ||||
|                         const n = new OsmNode(change.id) | ||||
|                         n.lat = change.changes["lat"] | ||||
|                         n.lon = change.changes["lon"] | ||||
|                         osmObj = n | ||||
|                         break; | ||||
|                     case "way": | ||||
|                         const w = new OsmWay(change.id) | ||||
|                         w.nodes = change.changes["nodes"] | ||||
|                         osmObj = w | ||||
|                         break; | ||||
|                     case "relation": | ||||
|                         const r = new OsmRelation(change.id) | ||||
|                         r.members = change.changes["members"] | ||||
|                         osmObj = r | ||||
|                         break; | ||||
|                 } | ||||
|                 if (osmObj === undefined) { | ||||
|                     throw "Hmm? This is a bug" | ||||
|                 } | ||||
|                 objects.set(id, osmObj) | ||||
|             } | ||||
| 
 | ||||
|             const state = states.get(id) | ||||
|             if (change.doDelete) { | ||||
|                 if (state === "created") { | ||||
|                     states.set(id, "unchanged") | ||||
|                 } else { | ||||
|                     states.set(id, "deleted") | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const obj = objects.get(id) | ||||
|             // Apply tag changes
 | ||||
|             for (const kv of change.tags ?? []) { | ||||
|                 const k = kv.k | ||||
|                 let v = kv.v | ||||
| 
 | ||||
|                 if (v === "") { | ||||
|                     v = undefined; | ||||
|                 } | ||||
| 
 | ||||
|                 const oldV = obj.type[k] | ||||
|                 if (oldV === v) { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 obj.tags[k] = v; | ||||
|                 changed = true; | ||||
| 
 | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
|             if (change.changes !== undefined) { | ||||
|                 switch (change.type) { | ||||
|                     case "node": | ||||
|                         // @ts-ignore
 | ||||
|                         const nlat = change.changes.lat; | ||||
|                         // @ts-ignore
 | ||||
|                         const nlon = change.changes.lon; | ||||
|                         const n = <OsmNode>obj | ||||
|                         if (n.lat !== nlat || n.lon !== nlon) { | ||||
|                             n.lat = nlat; | ||||
|                             n.lon = nlon; | ||||
|                             changed = true; | ||||
|                         } | ||||
|                         break; | ||||
|                     case "way": | ||||
|                         const nnodes = change.changes["nodes"] | ||||
|                         const w = <OsmWay>obj | ||||
|                         if (!Utils.Identical(nnodes, w.nodes)) { | ||||
|                             w.nodes = nnodes | ||||
|                             changed = true; | ||||
|                         } | ||||
|                         break; | ||||
|                     case "relation": | ||||
|                         const nmembers: { type: "node" | "way" | "relation", ref: number, role: string }[] = change.changes["members"] | ||||
|                         const r = <OsmRelation>obj | ||||
|                         if (!Utils.Identical(nmembers, r.members, (a, b) => { | ||||
|                             return a.role === b.role && a.type === b.type && a.ref === b.ref | ||||
|                         })) { | ||||
|                             r.members = nmembers; | ||||
|                             changed = true; | ||||
|                         } | ||||
|                         break; | ||||
| 
 | ||||
|                 } | ||||
|                 | ||||
|             } | ||||
| 
 | ||||
|             if (changed && state === "unchanged") { | ||||
|                 states.set(id, "modified") | ||||
|             } | ||||
|         } | ||||
|         this.pending.ping(); | ||||
|         eventSource.ping(); | ||||
| 
 | ||||
| 
 | ||||
|         const result = { | ||||
|             newObjects: [], | ||||
|             modifiedObjects: [], | ||||
|             deletedObjects: [] | ||||
| 
 | ||||
|         } | ||||
|         objects.forEach((v, id) => { | ||||
| 
 | ||||
|             const state = states.get(id) | ||||
|             if (state === "created") { | ||||
|                 result.newObjects.push(v) | ||||
|             } | ||||
|             if (state === "modified") { | ||||
|                 result.modifiedObjects.push(v) | ||||
|             } | ||||
|             if (state === "deleted") { | ||||
|                 result.deletedObjects.push(v) | ||||
|             } | ||||
| 
 | ||||
|         }) | ||||
| 
 | ||||
|         return result | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a new ID and updates the value for the next ID | ||||
|      */ | ||||
|     public getNewID() { | ||||
|         return Changes._nextId--; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | @ -83,121 +227,16 @@ export class Changes implements FeatureSource { | |||
|      * Triggered by the 'PendingChangeUploader'-actor in Actors | ||||
|      */ | ||||
|     public flushChanges(flushreason: string = undefined) { | ||||
|         if (this.pending.data.length === 0) { | ||||
|         if (this.pendingChanges.data.length === 0) { | ||||
|             console.log("No pending changes") | ||||
|             return; | ||||
|         } | ||||
|         if (flushreason !== undefined) { | ||||
|             console.log(flushreason) | ||||
|         } | ||||
|         this.uploadAll(); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Returns a new ID and updates the value for the next ID | ||||
|      */ | ||||
|     public getNewID(){ | ||||
|         return Changes._nextId--; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Create a new node element at the given lat/long. | ||||
|      * An internal OsmObject is created to upload later on, a geojson represention is returned. | ||||
|      * Note that the geojson version shares the tags (properties) by pointer, but has _no_ id in properties | ||||
|      */ | ||||
|     public createElement(basicTags: Tag[], lat: number, lon: number) { | ||||
|         console.log("Creating a new element with ", basicTags) | ||||
| 
 | ||||
|         const osmNode = new OsmNode(this.getNewID()); | ||||
|         const properties = {id: osmNode.id}; | ||||
|         const geojson = { | ||||
|             "type": "Feature", | ||||
|             "properties": properties, | ||||
|             "id": properties.id, | ||||
|             "geometry": { | ||||
|                 "type": "Point", | ||||
|                 "coordinates": [ | ||||
|                     lon, | ||||
|                     lat | ||||
|                 ] | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // The basictags are COPIED, the id is included in the properties
 | ||||
|         // The tags are not yet written into the OsmObject, but this is applied onto a 
 | ||||
|         const changes = []; | ||||
|         for (const kv of basicTags) { | ||||
|             if (typeof kv.value !== "string") { | ||||
|                 throw "Invalid value: don't use a regex in a preset" | ||||
|             } | ||||
|             properties[kv.key] = kv.value; | ||||
|             changes.push({elementId:properties.id, key: kv.key, value: kv.value}) | ||||
|         } | ||||
| 
 | ||||
|         console.log("New feature added and pinged") | ||||
|         this.features.data.push({feature: geojson, freshness: new Date()}); | ||||
|         this.features.ping(); | ||||
| 
 | ||||
|         State.state.allElements.addOrGetElement(geojson).ping(); | ||||
| 
 | ||||
|         if (State.state.osmConnection.userDetails.data.backend !== OsmConnection.oauth_configs.osm.url) { | ||||
|             properties["_backend"] = State.state.osmConnection.userDetails.data.backend | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         this.newObjects.data.push({id: osmNode.id, lat: lat, lon: lon}) | ||||
|         this.pending.data.push(...changes) | ||||
|         this.pending.ping(); | ||||
|         this.newObjects.ping(); | ||||
|         return geojson; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private uploadChangesWithLatestVersions( | ||||
|         knownElements: OsmObject[]) { | ||||
|         const knownById = new Map<string, OsmObject>(); | ||||
|         knownElements.forEach(knownElement => { | ||||
|             knownById.set(knownElement.type + "/" + knownElement.id, knownElement) | ||||
|         }) | ||||
| 
 | ||||
|         const newElements: OsmNode [] = this.newObjects.data.map(spec => { | ||||
|             const newElement = new OsmNode(spec.id); | ||||
|             newElement.lat = spec.lat; | ||||
|             newElement.lon = spec.lon; | ||||
|             return newElement | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         // Here, inside the continuation, we know that all 'neededIds' are loaded in 'knownElements', which maps the ids onto the elements
 | ||||
|         // We apply the changes on them
 | ||||
|         for (const change of this.pending.data) { | ||||
|             if (parseInt(change.elementId.split("/")[1]) < 0) { | ||||
|                 // This is a new element - we should apply this on one of the new elements
 | ||||
|                 for (const newElement of newElements) { | ||||
|                     if (newElement.type + "/" + newElement.id === change.elementId) { | ||||
|                         newElement.addTag(change.key, change.value); | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 knownById.get(change.elementId).addTag(change.key, change.value); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         // Small sanity check for duplicate information
 | ||||
|         let changedElements = []; | ||||
|         for (const elementId in knownElements) { | ||||
|             const element = knownElements[elementId]; | ||||
|             if (element.changed) { | ||||
|                 changedElements.push(element); | ||||
|             } | ||||
|         } | ||||
|         if (changedElements.length == 0 && newElements.length == 0) { | ||||
|             console.log("No changes in any object - clearing"); | ||||
|             this.pending.setData([]) | ||||
|             this.newObjects.setData([]) | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         if (this.isUploading.data) { | ||||
|             console.log("Is uploading... Abort") | ||||
|             return; | ||||
|         } | ||||
|         this.isUploading.setData(true) | ||||
|  | @ -205,75 +244,45 @@ export class Changes implements FeatureSource { | |||
|         console.log("Beginning upload..."); | ||||
|         // At last, we build the changeset and upload
 | ||||
|         const self = this; | ||||
|         State.state.osmConnection.UploadChangeset( | ||||
|             State.state.layoutToUse.data, | ||||
|             State.state.allElements, | ||||
|             (csId) =>   Changes.createChangesetFor(csId,changedElements, newElements ), | ||||
|             () => { | ||||
|                 // When done
 | ||||
|                 console.log("Upload successfull!") | ||||
|                 self.newObjects.setData([]) | ||||
|                 self.pending.setData([]); | ||||
|                 self.isUploading.setData(false) | ||||
|             }, | ||||
|             () => self.isUploading.setData(false) // Failed - mark to try again
 | ||||
|             ); | ||||
| 
 | ||||
|       | ||||
|     }; | ||||
| 
 | ||||
| 
 | ||||
|     public static createChangesetFor(csId: string, changedElements: OsmObject[], newElements: OsmObject[]): string { | ||||
| 
 | ||||
|         let modifications = ""; | ||||
|         for (const element of changedElements) { | ||||
|             modifications += element.ChangesetXML(csId) + "\n"; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         let creations = ""; | ||||
|         for (const newElement of newElements) { | ||||
|             creations += newElement.ChangesetXML(csId); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`; | ||||
| 
 | ||||
|         if (creations.length > 0) { | ||||
|             changes += | ||||
|                 "\n<create>\n" + | ||||
|                 creations + | ||||
|                 "</create>"; | ||||
|         } | ||||
|         if (modifications.length > 0) { | ||||
|             changes += | ||||
|                 "\n<modify>\n" + | ||||
|                 modifications + | ||||
|                 "\n</modify>"; | ||||
|         } | ||||
|          | ||||
|         changes += "</osmChange>"; | ||||
|         return changes; | ||||
|     } | ||||
| 
 | ||||
|     private uploadAll() { | ||||
|         const self = this; | ||||
| 
 | ||||
|         const pending = this.pending.data; | ||||
|         let neededIds: string[] = []; | ||||
|         for (const change of pending) { | ||||
|             const id = change.elementId; | ||||
|             if (parseFloat(id.split("/")[1]) < 0) { | ||||
|                 // New element - we don't have to download this
 | ||||
|             } else { | ||||
|                 neededIds.push(id); | ||||
|         const pending = self.pendingChanges.data; | ||||
|         const neededIds =  Changes.GetNeededIds(pending) | ||||
|         console.log("Needed ids", neededIds) | ||||
|         OsmObject.DownloadAll(neededIds, true).addCallbackAndRunD(osmObjects => { | ||||
|             console.log("Got the fresh objects!", osmObjects, "pending: ", pending) | ||||
|             const changes = Changes.CreateChangesetObjects(pending, osmObjects) | ||||
|             console.log("Changes", changes) | ||||
|             if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { | ||||
|                 console.log("No changes to be made") | ||||
|                 this.pendingChanges.setData([]) | ||||
|                 this.isUploading.setData(false) | ||||
|                 return; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         neededIds = Utils.Dedup(neededIds); | ||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||
|             self.uploadChangesWithLatestVersions(knownElements) | ||||
|         }) | ||||
| 
 | ||||
|             State.state.osmConnection.UploadChangeset( | ||||
|                 State.state.layoutToUse.data, | ||||
|                 State.state.allElements, | ||||
|                 (csId) => { | ||||
|                     return Changes.createChangesetFor(csId, changes); | ||||
|                 }, | ||||
|                 () => { | ||||
|                     // When done
 | ||||
|                     console.log("Upload successfull!") | ||||
|                     self.pendingChanges.setData([]); | ||||
|                     self.isUploading.setData(false) | ||||
|                 }, | ||||
|                 () => self.isUploading.setData(false) // Failed - mark to try again
 | ||||
|             ) | ||||
| 
 | ||||
|         }); | ||||
| 
 | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     public applyAction(action: OsmChangeAction) { | ||||
|         const changes = action.Perform(this) | ||||
|         console.log("Received changes:", changes) | ||||
|         this.pendingChanges.data.push(...changes); | ||||
|         this.pendingChanges.ping(); | ||||
|     } | ||||
| } | ||||
|  | @ -1,3 +0,0 @@ | |||
| export default class CreateNewNodeAction { | ||||
|      | ||||
| } | ||||
|  | @ -125,7 +125,7 @@ export abstract class OsmObject { | |||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = splitted[1]; | ||||
|         const idN = Number(splitted[1]); | ||||
|         const src = new UIEventSource<OsmObject[]>([]); | ||||
|         OsmObject.historyCache.set(id, src); | ||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => { | ||||
|  | @ -314,20 +314,6 @@ export abstract class OsmObject { | |||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public addTag(k: string, v: string): void { | ||||
|         if (k in this.tags) { | ||||
|             const oldV = this.tags[k]; | ||||
|             if (oldV == v) { | ||||
|                 return; | ||||
|             } | ||||
|             console.log("Overwriting ", oldV, " with ", v, " for key ", k) | ||||
|         } | ||||
|         this.tags[k] = v; | ||||
|         if (v === undefined || v === "") { | ||||
|             delete this.tags[k]; | ||||
|         } | ||||
|         this.changed = true; | ||||
|     } | ||||
| 
 | ||||
|     abstract ChangesetXML(changesetId: string): string; | ||||
| 
 | ||||
|  | @ -481,7 +467,11 @@ export class OsmWay extends OsmObject { | |||
| 
 | ||||
| export class OsmRelation extends OsmObject { | ||||
| 
 | ||||
|     public members; | ||||
|     public members: { | ||||
|         type: "node" | "way" | "relation", | ||||
|         ref: number, | ||||
|         role: string | ||||
|     }[]; | ||||
| 
 | ||||
|     constructor(id: number) { | ||||
|         super("relation", id); | ||||
|  |  | |||
|  | @ -1,11 +0,0 @@ | |||
| /** | ||||
|  * The logic to handle relations after a way within  | ||||
|  */ | ||||
| export default class RelationSplitlHandler { | ||||
| 
 | ||||
|     constructor() { | ||||
|          | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -1,222 +0,0 @@ | |||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | ||||
| import {GeoOperations} from "../GeoOperations"; | ||||
| import State from "../../State"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import {Changes} from "./Changes"; | ||||
| 
 | ||||
| interface SplitInfo { | ||||
|     originalIndex?: number, // or negative for new elements
 | ||||
|     lngLat: [number, number], | ||||
|     doSplit: boolean | ||||
| } | ||||
| 
 | ||||
| export default class SplitAction { | ||||
|     private readonly roadObject: any; | ||||
| 
 | ||||
|     /*** | ||||
|      * | ||||
|      * @param roadObject: the geojson of the road object. Properties.id must be the corresponding OSM-id | ||||
|      */ | ||||
|     constructor(roadObject: any) { | ||||
|         this.roadObject = roadObject; | ||||
|     } | ||||
| 
 | ||||
|     private static SegmentSplitInfo(splitInfo: SplitInfo[]): SplitInfo[][] { | ||||
|         const wayParts = [] | ||||
|         let currentPart = [] | ||||
|         for (const splitInfoElement of splitInfo) { | ||||
|             currentPart.push(splitInfoElement) | ||||
| 
 | ||||
|             if (splitInfoElement.doSplit) { | ||||
|                 // We have to do a split!
 | ||||
|                 // We add the current index to the currentParts, flush it and add it again
 | ||||
|                 wayParts.push(currentPart) | ||||
|                 currentPart = [splitInfoElement] | ||||
|             } | ||||
|         } | ||||
|         wayParts.push(currentPart) | ||||
|         return wayParts | ||||
|     } | ||||
| 
 | ||||
|     public DoSplit(splitPoints: any[]) { | ||||
|         // We mark the new split points with a new id
 | ||||
|         for (const splitPoint of splitPoints) { | ||||
|             splitPoint.properties["_is_split_point"] = true | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const self = this; | ||||
|         const id = this.roadObject.properties.id | ||||
|         const osmWay = <UIEventSource<OsmWay>>OsmObject.DownloadObject(id) | ||||
|         const partOf = OsmObject.DownloadReferencingRelations(id) | ||||
|         osmWay.map(originalElement => { | ||||
| 
 | ||||
|             if(originalElement === undefined || partOf === undefined){ | ||||
|                 return; | ||||
|             } | ||||
|              | ||||
|             const changes = State.state?.changes ?? new Changes(); | ||||
|             // First, calculate splitpoints and remove points close to one another
 | ||||
|             const splitInfo = self.CalculateSplitCoordinates(splitPoints) | ||||
|             // Now we have a list with e.g. 
 | ||||
|             // [ { originalIndex: 0}, {originalIndex: 1, doSplit: true}, {originalIndex: 2}, {originalIndex: undefined, doSplit: true}, {originalIndex: 3}]
 | ||||
| 
 | ||||
|             // Lets change 'originalIndex' to the actual node id first:
 | ||||
|             for (const element of splitInfo) { | ||||
|                 if (element.originalIndex >= 0) { | ||||
|                     element.originalIndex = originalElement.nodes[element.originalIndex] | ||||
|                 } else { | ||||
|                     element.originalIndex = changes.getNewID(); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Next up is creating actual parts from this
 | ||||
|             const wayParts = SplitAction.SegmentSplitInfo(splitInfo); | ||||
| 
 | ||||
|             // Allright! At this point, we have our new ways!
 | ||||
|             // Which one is the longest of them (and can keep the id)?
 | ||||
| 
 | ||||
|             let longest = undefined; | ||||
|             for (const wayPart of wayParts) { | ||||
|                 if (longest === undefined) { | ||||
|                     longest = wayPart; | ||||
|                     continue | ||||
|                 } | ||||
|                 if (wayPart.length > longest.length) { | ||||
|                     longest = wayPart | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             const newOsmObjects: OsmObject[] = [] | ||||
|             const modifiedObjects: OsmObject[] = [] | ||||
|             // Let's create the new points as needed
 | ||||
|             for (const element of splitInfo) { | ||||
|                 if (element.originalIndex >= 0) { | ||||
|                     continue; | ||||
|                 } | ||||
|                 const node = new OsmNode(element.originalIndex) | ||||
|                 node.lon = element.lngLat[0] | ||||
|                 node.lat = element.lngLat[1] | ||||
|                 newOsmObjects.push(node) | ||||
|             } | ||||
| 
 | ||||
|             const newWayIds: number[] = [] | ||||
|             // Lets create OsmWays based on them
 | ||||
|             for (const wayPart of wayParts) { | ||||
| 
 | ||||
|                 let isOriginal = wayPart === longest | ||||
|                 if(isOriginal){ | ||||
|                     // We change the actual element!
 | ||||
|                     originalElement.nodes = wayPart.map(p => p.originalIndex); | ||||
|                     originalElement.changed = true; | ||||
|                     modifiedObjects.push(originalElement) | ||||
|                 }else{ | ||||
|                     let id = changes.getNewID(); | ||||
|                     const way = new OsmWay(id) | ||||
|                     way.tags = originalElement.tags; | ||||
|                     way.nodes = wayPart.map(p => p.originalIndex); | ||||
|                     way.changed = true; | ||||
|                     newOsmObjects.push(way) | ||||
|                         newWayIds.push(way.id) | ||||
|                 } | ||||
|               | ||||
|             } | ||||
|              | ||||
|             // At last, we still have to check that we aren't part of a relation...
 | ||||
|             // At least, the order of the ways is identical, so we can keep the same roles
 | ||||
| 
 | ||||
|             modifiedObjects.push(...SplitAction.UpdateRelations(partOf.data, newWayIds, originalElement)) | ||||
|             // And we have our objects!
 | ||||
|             // Time to upload
 | ||||
| 
 | ||||
|             console.log(Changes.createChangesetFor("123", modifiedObjects, newOsmObjects)) | ||||
|         }, [partOf]) | ||||
|     } | ||||
| 
 | ||||
|     private static UpdateRelations(data: OsmRelation[], newWayIds: number[], originalElement: OsmWay):OsmRelation[]{ | ||||
|         // TODO
 | ||||
|         return [] | ||||
|     } | ||||
|      | ||||
|     /** | ||||
|      * Calculates the actual points to split | ||||
|      * If another point is closer then ~5m, we reuse that point | ||||
|      */ | ||||
|     private CalculateSplitCoordinates( | ||||
|         splitPoints: any[], | ||||
|         toleranceInM = 5): SplitInfo[] { | ||||
| 
 | ||||
|         const allPoints = [...splitPoints]; | ||||
|         // We have a bunch of coordinates here: [ [lat, lon], [lat, lon], ...] ...
 | ||||
|         const originalPoints: [number, number][] = this.roadObject.geometry.coordinates | ||||
|         // We project them onto the line (which should yield pretty much the same point
 | ||||
|         for (let i = 0; i < originalPoints.length; i++) { | ||||
|             let originalPoint = originalPoints[i]; | ||||
|             let projected = GeoOperations.nearestPoint(this.roadObject, originalPoint) | ||||
|             projected.properties["_is_split_point"] = false | ||||
|             projected.properties["_original_index"] = i | ||||
|             allPoints.push(projected) | ||||
|         } | ||||
|         // At this point, we have a list of both the split point and the old points, with some properties to discriminate between them
 | ||||
|         // We sort this list so that the new points are at the same location
 | ||||
|         allPoints.sort((a, b) => a.properties.location - b.properties.location) | ||||
| 
 | ||||
|         // When this is done, we check that no now point is too close to an already existing point and no very small segments get created
 | ||||
| 
 | ||||
|         for (let i = allPoints.length - 1; i > 0; i--) { | ||||
| 
 | ||||
|             const point = allPoints[i]; | ||||
|             if (point.properties._original_index !== undefined) { | ||||
|                 // This point is already in OSM - we have to keep it!
 | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if (i != allPoints.length - 1) { | ||||
|                 const prevPoint = allPoints[i + 1] | ||||
|                 const diff = Math.abs(point.properties.location - prevPoint.properties.location) * 1000 | ||||
|                 if (diff <= toleranceInM) { | ||||
|                     // To close to the previous point! We delete this point...
 | ||||
|                     allPoints.splice(i, 1) | ||||
|                     // ... and mark the previous point as a split point
 | ||||
|                     prevPoint.properties._is_split_point = true | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             if (i > 0) { | ||||
|                 const nextPoint = allPoints[i - 1] | ||||
|                 const diff = Math.abs(point.properties.location - nextPoint.properties.location) * 1000 | ||||
|                 if (diff <= toleranceInM) { | ||||
|                     // To close to the next point! We delete this point...
 | ||||
|                     allPoints.splice(i, 1) | ||||
|                     // ... and mark the next point as a split point
 | ||||
|                     nextPoint.properties._is_split_point = true | ||||
|                     // noinspection UnnecessaryContinueJS
 | ||||
|                     continue; | ||||
|                 } | ||||
|             } | ||||
|             // We don't have to remove this point...
 | ||||
|         } | ||||
| 
 | ||||
|         const splitInfo: SplitInfo[] = [] | ||||
|         let nextId = -1 | ||||
| 
 | ||||
|         for (const p of allPoints) { | ||||
|             let index = p.properties._original_index | ||||
|             if (index === undefined) { | ||||
|                 index = nextId; | ||||
|                 nextId--; | ||||
|             } | ||||
|             const splitInfoElement = { | ||||
|                 originalIndex: index, | ||||
|                 lngLat: p.geometry.coordinates, | ||||
|                 doSplit: p.properties._is_split_point | ||||
|             } | ||||
|             splitInfo.push(splitInfoElement) | ||||
|         } | ||||
| 
 | ||||
|         return splitInfo | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  | @ -2,21 +2,27 @@ import {Utils} from "../Utils"; | |||
| 
 | ||||
| export class UIEventSource<T> { | ||||
| 
 | ||||
|     private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||
|     public data: T; | ||||
|     public trace: boolean; | ||||
|     private readonly tag: string; | ||||
|     private _callbacks = []; | ||||
| 
 | ||||
|     private static allSources : UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         this.tag = tag; | ||||
|         this.data = data; | ||||
|         UIEventSource.allSources.push(this); | ||||
|     } | ||||
| 
 | ||||
|     static PrepPerf() : UIEventSource<any>[]{ | ||||
|         if(Utils.runningFromConsole){ | ||||
|     static PrepPerf(): UIEventSource<any>[] { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return []; | ||||
|         } | ||||
|         // @ts-ignore
 | ||||
|         window.mapcomplete_performance = () => { | ||||
|             console.log(UIEventSource.allSources.length, "uieventsources created"); | ||||
|             const copy = [...UIEventSource.allSources]; | ||||
|             copy.sort((a,b) => b._callbacks.length - a._callbacks.length); | ||||
|             copy.sort((a, b) => b._callbacks.length - a._callbacks.length); | ||||
|             console.log("Topten is:") | ||||
|             for (let i = 0; i < 10; i++) { | ||||
|                 console.log(copy[i].tag, copy[i]); | ||||
|  | @ -26,12 +32,6 @@ export class UIEventSource<T> { | |||
|         return []; | ||||
|     } | ||||
|      | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         this.tag = tag; | ||||
|         this.data = data; | ||||
|         UIEventSource.allSources.push(this); | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> { | ||||
|         const sink = new UIEventSource<X>(source.data?.data); | ||||
| 
 | ||||
|  | @ -68,6 +68,9 @@ export class UIEventSource<T> { | |||
|             // This ^^^ actually works!
 | ||||
|             throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." | ||||
|         } | ||||
|         if (this.trace) { | ||||
|             console.trace("Added a callback") | ||||
|         } | ||||
|         this._callbacks.push(callback); | ||||
|         return this; | ||||
|     } | ||||
|  | @ -101,12 +104,12 @@ export class UIEventSource<T> { | |||
|      */ | ||||
|     public map<J>(f: ((t: T) => J), | ||||
|                   extraSources: UIEventSource<any>[] = [], | ||||
|                   g: ((j:J, t:T) => T) = undefined): UIEventSource<J> { | ||||
|                   g: ((j: J, t: T) => T) = undefined): UIEventSource<J> { | ||||
|         const self = this; | ||||
| 
 | ||||
|         const newSource = new UIEventSource<J>( | ||||
|             f(this.data), | ||||
|             "map("+this.tag+")" | ||||
|             "map(" + this.tag + ")" | ||||
|         ); | ||||
| 
 | ||||
|         const update = function () { | ||||
|  | @ -159,9 +162,9 @@ export class UIEventSource<T> { | |||
|         return newSource; | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRunD(callback: (data :T ) => void) { | ||||
|     addCallbackAndRunD(callback: (data: T) => void) { | ||||
|         this.addCallbackAndRun(data => { | ||||
|             if(data !== undefined && data !== null){ | ||||
|             if (data !== undefined && data !== null) { | ||||
|                 callback(data) | ||||
|             } | ||||
|         }) | ||||
|  |  | |||
							
								
								
									
										4
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										4
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -19,6 +19,7 @@ import TitleHandler from "./Logic/Actors/TitleHandler"; | |||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||
| import {Relation} from "./Logic/Osm/ExtractRelations"; | ||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | ||||
| import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  | @ -244,6 +245,9 @@ export default class State { | |||
| 
 | ||||
|         this.allElements = new ElementStorage(); | ||||
|         this.changes = new Changes(); | ||||
|          | ||||
|         new ChangeToElementsActor(this.changes, this.allElements) | ||||
|          | ||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource() | ||||
|          | ||||
|         new PendingChangesUploader(this.changes, this.selectedElement); | ||||
|  |  | |||
|  | @ -39,6 +39,7 @@ export default class Minimap extends BaseUIElement { | |||
|         div.style.width = "100%" | ||||
|         div.style.minWidth = "40px" | ||||
|         div.style.minHeight = "40px" | ||||
|         div.style.position = "relative" | ||||
|         const wrapper = document.createElement("div") | ||||
|         wrapper.appendChild(div) | ||||
|         const self = this; | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ import LocationInput from "../Input/LocationInput"; | |||
| import {InputElement} from "../Input/InputElement"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||
| 
 | ||||
| /* | ||||
| * The SimpleAddUI is a single panel, which can have multiple states: | ||||
|  | @ -61,11 +62,6 @@ export default class SimpleAddUI extends Toggle { | |||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined); | ||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 | ||||
| 
 | ||||
|         function createNewPoint(tags: any[], location: { lat: number, lon: number }) { | ||||
|             let feature = State.state.changes.createElement(tags, location.lat, location.lon); | ||||
|             State.state.selectedElement.setData(feature); | ||||
|         } | ||||
| 
 | ||||
|         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) | ||||
| 
 | ||||
|         const addUi = new VariableUiElement( | ||||
|  | @ -75,7 +71,9 @@ export default class SimpleAddUI extends Toggle { | |||
|                     } | ||||
|                     return SimpleAddUI.CreateConfirmButton(preset, | ||||
|                         (tags, location) => { | ||||
|                             createNewPoint(tags, location) | ||||
|                             let changes = | ||||
|                                 State.state.changes.applyAction(new CreateNewNodeAction(tags, location.lat, location.lon)) | ||||
|                             State.state.selectedElement.setData(changes.newFeatures[0]); | ||||
|                             selectedPreset.setData(undefined) | ||||
|                         }, () => { | ||||
|                             selectedPreset.setData(undefined) | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import Combine from "../Base/Combine"; | |||
| import State from "../../State"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| 
 | ||||
| export default class DeleteImage extends Toggle { | ||||
|  | @ -15,14 +16,17 @@ export default class DeleteImage extends Toggle { | |||
|             .SetClass("rounded-full p-1") | ||||
|             .SetStyle("color:white;background:#ff8c8c") | ||||
|             .onClick(() => { | ||||
|                 State.state?.changes?.addTag(tags.data.id, new Tag(key, oldValue), tags); | ||||
|                 State.state?.changes?. | ||||
|                     applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data)) | ||||
|             }); | ||||
| 
 | ||||
|         const deleteButton = Translations.t.image.doDelete.Clone() | ||||
|             .SetClass("block w-full pl-4 pr-4") | ||||
|             .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;") | ||||
|             .onClick(() => { | ||||
|                 State.state?.changes?.addTag(tags.data.id, new Tag(key, ""), tags); | ||||
|                 State.state?.changes?.applyAction( | ||||
|                 new ChangeTagAction(    tags.data.id, new Tag(key, ""), tags.data) | ||||
|                 ) | ||||
|             }); | ||||
| 
 | ||||
|         const cancelButton = Translations.t.general.cancel.Clone().SetClass("bg-white pl-4 pr-4").SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;"); | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import FileSelectorButton from "../Input/FileSelectorButton"; | |||
| import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; | ||||
| import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; | ||||
| import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| export class ImageUploadFlow extends Toggle { | ||||
| 
 | ||||
|  | @ -28,7 +29,10 @@ export class ImageUploadFlow extends Toggle { | |||
|                 key = imagePrefix + ":" + freeIndex; | ||||
|             } | ||||
|             console.log("Adding image:" + key, url); | ||||
|             State.state.changes.addTag(tags.id, new Tag(key, url), tagsSource); | ||||
|             State.state.changes | ||||
|                 .applyAction(new ChangeTagAction( | ||||
|                     tags.id, new Tag(key, url), tagsSource.data | ||||
|                 )) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import State from "../../State"; | |||
| import Toggle from "../Input/Toggle"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Svg from "../../Svg"; | ||||
| import DeleteAction from "../../Logic/Osm/DeleteAction"; | ||||
| import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||
|  | @ -19,6 +19,7 @@ import {Changes} from "../../Logic/Osm/Changes"; | |||
| import {And} from "../../Logic/Tags/And"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import DeleteConfig from "../../Customizations/JSON/DeleteConfig"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| export default class DeleteWizard extends Toggle { | ||||
|     /** | ||||
|  | @ -58,7 +59,9 @@ export default class DeleteWizard extends Toggle { | |||
|                 }) | ||||
|             } | ||||
|             (State.state?.changes ?? new Changes()) | ||||
|                 .addTag(id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource); | ||||
|                 .applyAction(new ChangeTagAction( | ||||
|                    id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource.data | ||||
|                 )) | ||||
|         } | ||||
| 
 | ||||
|         function doDelete(selected: TagsFilter) { | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import DeleteWizard from "./DeleteWizard"; | ||||
| import SplitRoadWizard from "./SplitRoadWizard"; | ||||
| 
 | ||||
| export default class FeatureInfoBox extends ScrollableFullScreen { | ||||
| 
 | ||||
|  | @ -66,10 +67,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|             renderings.push(questionBox); | ||||
|         } | ||||
| 
 | ||||
|         const hasMinimap = layerConfig.tagRenderings.some(tr => tr.hasMinimap()) | ||||
|         if (!hasMinimap) { | ||||
|             renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) | ||||
|         } | ||||
| 
 | ||||
|         if (layerConfig.deletion) { | ||||
|             renderings.push( | ||||
|  | @ -81,6 +78,19 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|                 )) | ||||
|         } | ||||
| 
 | ||||
|         if (layerConfig.allowSplit) { | ||||
|             renderings.push( | ||||
|                 new VariableUiElement(tags.map(tags => tags.id).map(id => | ||||
|                     new SplitRoadWizard(id)) | ||||
|                 )) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const hasMinimap = layerConfig.tagRenderings.some(tr => tr.hasMinimap()) | ||||
|         if (!hasMinimap) { | ||||
|             renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) | ||||
|         } | ||||
| 
 | ||||
|         renderings.push( | ||||
|             new VariableUiElement( | ||||
|                 State.state.osmConnection.userDetails | ||||
|  |  | |||
|  | @ -11,7 +11,9 @@ import Combine from "../Base/Combine"; | |||
| import {Button} from "../Base/Button"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import SplitAction from "../../Logic/Osm/SplitAction"; | ||||
| import SplitAction from "../../Logic/Osm/Actions/SplitAction"; | ||||
| import {OsmObject, OsmWay} from "../../Logic/Osm/OsmObject"; | ||||
| import Title from "../Base/Title"; | ||||
| 
 | ||||
| export default class SplitRoadWizard extends Toggle { | ||||
|     private static splitLayout = new UIEventSource(SplitRoadWizard.GetSplitLayout()) | ||||
|  | @ -26,24 +28,25 @@ export default class SplitRoadWizard extends Toggle { | |||
|         const t = Translations.t.split; | ||||
| 
 | ||||
|         // Contains the points on the road that are selected to split on - contains geojson points with extra properties such as 'location' with the distance along the linestring
 | ||||
|         const splitPoints = new UIEventSource<{feature: any, freshness: Date}[]>([]); | ||||
|         const splitPoints = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
| 
 | ||||
|         const hasBeenSplit = new UIEventSource(false) | ||||
| 
 | ||||
|         // Toggle variable between show split button and map
 | ||||
|         const splitClicked = new UIEventSource<boolean>(false); | ||||
| 
 | ||||
|         // Minimap on which you can select the points to be splitted
 | ||||
|         const miniMap = new Minimap({background: State.state.backgroundLayer}); | ||||
|         miniMap.SetStyle("width: 100%; height: 50rem;"); | ||||
|         const miniMap = new Minimap({background: State.state.backgroundLayer, allowMoving: false}); | ||||
|         miniMap.SetStyle("width: 100%; height: 24rem;"); | ||||
| 
 | ||||
|         // Define how a cut is displayed on the map
 | ||||
| 
 | ||||
|         // Load the road with given id on the minimap
 | ||||
|         const roadElement = State.state.allElements.ContainingFeatures.get(id) | ||||
|         const splitAction = new SplitAction(roadElement) | ||||
|         const roadEventSource = new UIEventSource([{feature: roadElement, freshness: new Date()}]); | ||||
|         // Datalayer displaying the road and the cut points (if any)
 | ||||
|         new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true); | ||||
|         new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false) | ||||
|         new ShowDataLayer(roadEventSource, miniMap.leafletMap, State.state.layoutToUse, false, true, "splitRoadWay"); | ||||
|         new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false, "splitRoad: splitpoints") | ||||
| 
 | ||||
|         /** | ||||
|          * Handles a click on the overleaf map. | ||||
|  | @ -73,7 +76,7 @@ export default class SplitRoadWizard extends Toggle { | |||
|             })) | ||||
| 
 | ||||
|         // Toggle between splitmap
 | ||||
|         const splitButton = new SubtleButton(Svg.scissors_ui(), "Split road"); | ||||
|         const splitButton = new SubtleButton(Svg.scissors_ui(), t.inviteToSplit.Clone()); | ||||
|         splitButton.onClick( | ||||
|             () => { | ||||
|                 splitClicked.setData(true) | ||||
|  | @ -83,45 +86,63 @@ export default class SplitRoadWizard extends Toggle { | |||
|         // Only show the splitButton if logged in, else show login prompt
 | ||||
|         const splitToggle = new Toggle( | ||||
|             splitButton, | ||||
|             t.loginToSplit.Clone().onClick(State.state.osmConnection.AttemptLogin), | ||||
|             t.loginToSplit.Clone().onClick(() => State.state.osmConnection.AttemptLogin()), | ||||
|             State.state.osmConnection.isLoggedIn) | ||||
| 
 | ||||
|         // Save button
 | ||||
|         const saveButton = new Button("Split here", () => splitAction.DoSplit(splitPoints.data)); | ||||
|         const saveButton = new Button(t.split.Clone(), () => { | ||||
|             hasBeenSplit.setData(true) | ||||
|             OsmObject.DownloadObject(id).addCallbackAndRunD(way => { | ||||
|                     OsmObject.DownloadReferencingRelations(id).addCallbackAndRunD( | ||||
|                         partOf => { | ||||
|                             const splitAction = new SplitAction( | ||||
|                                 <OsmWay>way, way.asGeoJson(), partOf, splitPoints.data.map(ff => ff.feature) | ||||
|                             ) | ||||
|                             State.state.changes.applyAction(splitAction) | ||||
|                         } | ||||
|                     ) | ||||
| 
 | ||||
|                 } | ||||
|             ) | ||||
| 
 | ||||
| 
 | ||||
|         }); | ||||
|         saveButton.SetClass("block btn btn-primary"); | ||||
|         const disabledSaveButton = new Button("Split here", undefined); | ||||
|         disabledSaveButton.SetClass("block btn btn-disabled"); | ||||
|         // Only show the save button if there are split points defined
 | ||||
|         const saveToggle = new Toggle(disabledSaveButton, saveButton, splitPoints.map((data) => data.length === 0)) | ||||
| 
 | ||||
|         const cancelButton = new Button("Cancel", () => { | ||||
|         const cancelButton = new Button(Translations.t.general.cancel.Clone(), () => { | ||||
|             splitClicked.setData(false); | ||||
|             splitPoints.setData([]); | ||||
|             splitClicked.setData(false) | ||||
|         }); | ||||
| 
 | ||||
|         cancelButton.SetClass("block btn btn-secondary"); | ||||
| 
 | ||||
|         const splitTitle = t.splitTitle; | ||||
| 
 | ||||
|         const mapView = new Combine([splitTitle, miniMap, cancelButton, saveToggle]); | ||||
|         super(mapView, splitToggle, splitClicked); | ||||
|         const splitTitle = new Title(t.splitTitle); | ||||
| 
 | ||||
|         const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle])]); | ||||
|         mapView.SetClass("question") | ||||
|         const confirm = new Toggle(mapView, splitToggle, splitClicked); | ||||
|         super(t.hasBeenSplit.Clone(), confirm, hasBeenSplit) | ||||
|     } | ||||
| 
 | ||||
|     private static GetSplitLayout(): LayoutConfig { | ||||
|         return new LayoutConfig({ | ||||
|             maintainer: "mapcomplete", | ||||
|             language: [], | ||||
|             language: ["en"], | ||||
|             startLon: 0, | ||||
|             startLat: 0, | ||||
|             description: undefined, | ||||
|             description: "Split points visualisations - built in at SplitRoadWizard.ts", | ||||
|             icon: "", startZoom: 0, | ||||
|             title: "Split locations", | ||||
|             version: "", | ||||
| 
 | ||||
|             id: "splitpositions", | ||||
|             layers: [{id: "splitpositions", source: {osmTags: "_cutposition=yes"}, icon: "./assets/svg/plus.svg"}] | ||||
|         }, true, "split road wizard layout") | ||||
|         }, true, "(BUILTIN) SplitRoadWizard.ts") | ||||
| 
 | ||||
|     } | ||||
| } | ||||
|  | @ -25,6 +25,7 @@ import BaseUIElement from "../BaseUIElement"; | |||
| import {DropDown} from "../Input/DropDown"; | ||||
| import {Unit} from "../../Customizations/JSON/Denomination"; | ||||
| import InputElementWrapper from "../Input/InputElementWrapper"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| /** | ||||
|  * Shows the question element. | ||||
|  | @ -56,7 +57,9 @@ export default class TagRenderingQuestion extends Combine { | |||
|             const selection = inputElement.GetValue().data; | ||||
|             if (selection) { | ||||
|                 (State.state?.changes ?? new Changes()) | ||||
|                     .addTag(tags.data.id, selection, tags); | ||||
|                     .applyAction(new ChangeTagAction( | ||||
|                         tags.data.id, selection, tags.data | ||||
|                     )) | ||||
|             } | ||||
| 
 | ||||
|             if (options.afterSave) { | ||||
|  |  | |||
|  | @ -22,7 +22,8 @@ export default class ShowDataLayer { | |||
|                 leafletMap: UIEventSource<L.Map>, | ||||
|                 layoutToUse: UIEventSource<LayoutConfig>, | ||||
|                 enablePopups = true, | ||||
|                 zoomToFeatures = false) { | ||||
|                 zoomToFeatures = false, | ||||
|                 name?:string) { | ||||
|         this._leafletMap = leafletMap; | ||||
|         this._enablePopups = enablePopups; | ||||
|         this._features = features; | ||||
|  | @ -60,6 +61,7 @@ export default class ShowDataLayer { | |||
|             } | ||||
| 
 | ||||
|             const allFeats = features.data.map(ff => ff.feature); | ||||
|             console.log("Rendering ",allFeats, "features at layer ", name) | ||||
|             geoLayer = self.CreateGeojsonLayer(); | ||||
|             for (const feat of allFeats) { | ||||
|                 if (feat === undefined) { | ||||
|  | @ -85,9 +87,6 @@ export default class ShowDataLayer { | |||
|                     console.error(e) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             State.state.selectedElement.ping(); | ||||
|         } | ||||
| 
 | ||||
|         features.addCallback(() => update()); | ||||
|  |  | |||
|  | @ -126,6 +126,7 @@ export default class SpecialVisualizations { | |||
|                                 // This is a list of values
 | ||||
|                                 idList = JSON.parse(value) | ||||
|                             } | ||||
|                              | ||||
|                             for (const id of idList) { | ||||
|                                 features.push({ | ||||
|                                     freshness: new Date(), | ||||
|  |  | |||
							
								
								
									
										13
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -136,6 +136,19 @@ export class Utils { | |||
|         return newArr; | ||||
|     } | ||||
|      | ||||
|     public static Identical<T>(t1: T[], t2: T[], eq?: (t: T, t0: T) => boolean): boolean{ | ||||
|         if(t1.length !== t2.length){ | ||||
|             return false | ||||
|         } | ||||
|         eq = (a, b) => a === b | ||||
|         for (let i = 0; i < t1.length ; i++) { | ||||
|             if(!eq(t1[i] ,t2[i])){ | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     public static MergeTags(a: any, b: any) { | ||||
|         const t = {}; | ||||
|         for (const k in a) { | ||||
|  |  | |||
|  | @ -273,7 +273,8 @@ | |||
|       }, | ||||
|       "tagRenderings": [ | ||||
|         "images" | ||||
|       ] | ||||
|       ], | ||||
|       "allowSplit": true | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -30,8 +30,10 @@ | |||
|   "split": { | ||||
|     "split": "Split", | ||||
|     "cancel": "Cancel", | ||||
|     "inviteToSplit": "Split this road", | ||||
|     "loginToSplit": "You must be logged in to split a road", | ||||
|     "splitTitle": "Choose on the map where to split this road" | ||||
|     "splitTitle": "Choose on the map where to split this road", | ||||
|     "hasBeenSplit": "This way has been split" | ||||
|   }, | ||||
|   "delete": { | ||||
|     "delete": "Delete", | ||||
|  |  | |||
							
								
								
									
										2
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,4 +1,4 @@ | |||
| import SplitAction from "./Logic/Osm/SplitAction"; | ||||
| import SplitAction from "./Logic/Osm/Actions/SplitAction"; | ||||
| import {GeoOperations} from "./Logic/GeoOperations"; | ||||
| 
 | ||||
| const way = { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue