forked from MapComplete/MapComplete
		
	Merge develop
This commit is contained in:
		
						commit
						c8bd412476
					
				
					 49 changed files with 1342 additions and 977 deletions
				
			
		|  | @ -49,6 +49,7 @@ export default class LayerConfig { | ||||||
|     wayHandling: number; |     wayHandling: number; | ||||||
|     public readonly units: Unit[]; |     public readonly units: Unit[]; | ||||||
|     public readonly deletion: DeleteConfig | null; |     public readonly deletion: DeleteConfig | null; | ||||||
|  |     public readonly allowSplit: boolean | ||||||
| 
 | 
 | ||||||
|     presets: { |     presets: { | ||||||
|         title: Translation, |         title: Translation, | ||||||
|  | @ -70,6 +71,7 @@ export default class LayerConfig { | ||||||
|         context = context + "." + json.id; |         context = context + "." + json.id; | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.id = json.id; |         this.id = json.id; | ||||||
|  |         this.allowSplit = json.allowSplit ?? false; | ||||||
|         this.name = Translations.T(json.name, context + ".name"); |         this.name = Translations.T(json.name, context + ".name"); | ||||||
| 
 | 
 | ||||||
|         if (json.description !== undefined) { |         if (json.description !== undefined) { | ||||||
|  | @ -373,8 +375,7 @@ export default class LayerConfig { | ||||||
| 
 | 
 | ||||||
|     public GenerateLeafletStyle( |     public GenerateLeafletStyle( | ||||||
|         tags: UIEventSource<any>, |         tags: UIEventSource<any>, | ||||||
|         clickable: boolean, |         clickable: boolean | ||||||
|         widthHeight = "100%" |  | ||||||
|     ): { |     ): { | ||||||
|         icon: { |         icon: { | ||||||
|             html: BaseUIElement; |             html: BaseUIElement; | ||||||
|  |  | ||||||
|  | @ -298,4 +298,9 @@ export interface LayerConfigJson { | ||||||
|      */ |      */ | ||||||
|     deletion?: boolean | DeleteConfigJson |     deletion?: boolean | DeleteConfigJson | ||||||
| 
 | 
 | ||||||
|  |     /** | ||||||
|  |      * IF set, a 'split this road' button is shown | ||||||
|  |      */ | ||||||
|  |     allowSplit?: boolean | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | @ -66,7 +66,7 @@ export default class LayoutConfig { | ||||||
|             this.language = json.language; |             this.language = json.language; | ||||||
|         } |         } | ||||||
|         if (this.language.length == 0) { |         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) { |         if (json.title === undefined) { | ||||||
|             throw "Title not defined in " + this.id; |             throw "Title not defined in " + this.id; | ||||||
|  |  | ||||||
|  | @ -513,10 +513,10 @@ export class InitUiElements { | ||||||
| 
 | 
 | ||||||
|         const source = new FeaturePipeline( |         const source = new FeaturePipeline( | ||||||
|             state.filteredLayers, |             state.filteredLayers, | ||||||
|  |             State.state.changes, | ||||||
|             updater, |             updater, | ||||||
|             state.osmApiFeatureSource, |             state.osmApiFeatureSource, | ||||||
|             state.layoutToUse, |             state.layoutToUse, | ||||||
|             state.changes, |  | ||||||
|             state.locationControl, |             state.locationControl, | ||||||
|             state.selectedElement |             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; // Ignored as the geometryFixer will introduce this
 | ||||||
|  |                 } | ||||||
|  |                 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>) { |     constructor(changes: Changes, selectedFeature: UIEventSource<any>) { | ||||||
|         const self = this; |         const self = this; | ||||||
|         this.lastChange = new Date(); |         this.lastChange = new Date(); | ||||||
|         changes.pending.addCallback(() => { |         changes.pendingChanges.addCallback(() => { | ||||||
|             self.lastChange = new Date(); |             self.lastChange = new Date(); | ||||||
| 
 | 
 | ||||||
|             window.setTimeout(() => { |             window.setTimeout(() => { | ||||||
|  | @ -54,7 +54,7 @@ export default class PendingChangesUploader { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         function onunload(e) { |         function onunload(e) { | ||||||
|             if (changes.pending.data.length == 0) { |             if(changes.pendingChanges.data.length == 0){ | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             changes.flushChanges("onbeforeunload - probably closing or something similar"); |             changes.flushChanges("onbeforeunload - probably closing or something similar"); | ||||||
|  |  | ||||||
|  | @ -13,7 +13,7 @@ export default class SelectedFeatureHandler { | ||||||
|     private readonly _hash: UIEventSource<string>; |     private readonly _hash: UIEventSource<string>; | ||||||
|     private readonly _selectedFeature: UIEventSource<any>; |     private readonly _selectedFeature: UIEventSource<any>; | ||||||
| 
 | 
 | ||||||
|     private static readonly _no_trigger_on = ["welcome","copyright","layers"] |     private static readonly _no_trigger_on = ["welcome","copyright","layers","new"] | ||||||
|     private readonly _osmApiSource: OsmApiFeatureSource; |     private readonly _osmApiSource: OsmApiFeatureSource; | ||||||
|      |      | ||||||
|     constructor(hash: UIEventSource<string>,  |     constructor(hash: UIEventSource<string>,  | ||||||
|  | @ -60,7 +60,9 @@ export default class SelectedFeatureHandler { | ||||||
|         if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){ |         if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){ | ||||||
|             return; // No valid feature selected
 |             return; // No valid feature selected
 | ||||||
|         } |         } | ||||||
|         // We should have a valid osm-ID and zoom to it
 |         // We should have a valid osm-ID and zoom to it... But we wrap it in try-catch to be sure
 | ||||||
|  |         try{ | ||||||
|  |              | ||||||
|         OsmObject.DownloadObject(hash).addCallbackAndRunD(element => { |         OsmObject.DownloadObject(hash).addCallbackAndRunD(element => { | ||||||
|             const centerpoint = element.centerpoint(); |             const centerpoint = element.centerpoint(); | ||||||
|             console.log("Zooming to location for select point: ", centerpoint) |             console.log("Zooming to location for select point: ", centerpoint) | ||||||
|  | @ -68,6 +70,9 @@ export default class SelectedFeatureHandler { | ||||||
|             location.data.lon = centerpoint[1] |             location.data.lon = centerpoint[1] | ||||||
|             location.ping(); |             location.ping(); | ||||||
|         }) |         }) | ||||||
|  |         }catch(e){ | ||||||
|  |             console.error("Could not download OSM-object with id", hash, " - probably a weird hash") | ||||||
|  |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|     private downloadFeature(hash:  string){ |     private downloadFeature(hash:  string){ | ||||||
|  |  | ||||||
							
								
								
									
										162
									
								
								Logic/FeatureSource/ChangeApplicator.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								Logic/FeatureSource/ChangeApplicator.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,162 @@ | ||||||
|  | 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, mode?: { | ||||||
|  |         generateNewGeometries: boolean | ||||||
|  |     }) { | ||||||
|  | 
 | ||||||
|  |         this.name = "ChangesApplied(" + source.name + ")" | ||||||
|  |         this.features = source.features | ||||||
|  |         const seenChanges = new Set<ChangeDescription>(); | ||||||
|  |         const self = this; | ||||||
|  |         let runningUpdate = false; | ||||||
|  |         source.features.addCallbackAndRunD(features => { | ||||||
|  |             if (runningUpdate) { | ||||||
|  |                 return; // No need to ping again
 | ||||||
|  |             } | ||||||
|  |             ChangeApplicator.ApplyChanges(features, changes.pendingChanges.data, mode) | ||||||
|  |             seenChanges.clear() | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         changes.pendingChanges.addCallbackAndRunD(changes => { | ||||||
|  |             runningUpdate = true; | ||||||
|  |             changes = changes.filter(ch => !seenChanges.has(ch)) | ||||||
|  |             changes.forEach(c => seenChanges.add(c)) | ||||||
|  |             ChangeApplicator.ApplyChanges(self.features.data, changes, mode) | ||||||
|  |             source.features.ping() | ||||||
|  |             runningUpdate = false; | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns true if the geometry is changed and the source should be pinged | ||||||
|  |      */ | ||||||
|  |     private static ApplyChanges(features: { feature: any; freshness: Date }[], cs: ChangeDescription[], mode: { generateNewGeometries: boolean }): boolean { | ||||||
|  |         if (cs.length === 0 || features === undefined) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log("Applying changes ", this.name, cs) | ||||||
|  |         let geometryChanged = false; | ||||||
|  |         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) { | ||||||
|  |             feature.id = feature.properties.id | ||||||
|  |             features.push({ | ||||||
|  |                 feature: feature, | ||||||
|  |                 freshness: now | ||||||
|  |             }) | ||||||
|  |             console.log("Added a new feature: ", feature) | ||||||
|  |             geometryChanged = true; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // First, create the new features - they have a negative ID
 | ||||||
|  |         // We don't set the properties yet though
 | ||||||
|  |         if (mode?.generateNewGeometries) { | ||||||
|  |             changesPerId.forEach(cs => { | ||||||
|  |                 cs | ||||||
|  |                     .forEach(change => { | ||||||
|  |                         if (change.id >= 0) { | ||||||
|  |                             return; // Nothing to do here, already created
 | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         if (change.changes === undefined) { | ||||||
|  |                             // An update to the object - not the actual created
 | ||||||
|  |                             return; | ||||||
|  |                         } | ||||||
|  | 
 | ||||||
|  |                         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 f = feature.feature; | ||||||
|  |             const id = f.properties.id; | ||||||
|  |             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
 | ||||||
|  |                     f.properties[kv.k] = kv.v; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Apply other changes to the object
 | ||||||
|  |                 if (change.changes !== undefined) { | ||||||
|  |                     geometryChanged = true; | ||||||
|  |                     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; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         return geometryChanged | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -6,7 +6,6 @@ import FeatureDuplicatorPerLayer from "../FeatureSource/FeatureDuplicatorPerLaye | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | import FeatureSource from "../FeatureSource/FeatureSource"; | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import LocalStorageSaver from "./LocalStorageSaver"; | import LocalStorageSaver from "./LocalStorageSaver"; | ||||||
| import LayerConfig from "../../Customizations/JSON/LayerConfig"; |  | ||||||
| import LocalStorageSource from "./LocalStorageSource"; | import LocalStorageSource from "./LocalStorageSource"; | ||||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc"; | ||||||
|  | @ -14,6 +13,8 @@ import GeoJsonSource from "./GeoJsonSource"; | ||||||
| import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource"; | import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource"; | ||||||
| import RegisteringFeatureSource from "./RegisteringFeatureSource"; | import RegisteringFeatureSource from "./RegisteringFeatureSource"; | ||||||
| import FilteredLayer from "../../Models/FilteredLayer"; | import FilteredLayer from "../../Models/FilteredLayer"; | ||||||
|  | import {Changes} from "../Osm/Changes"; | ||||||
|  | import ChangeApplicator from "./ChangeApplicator"; | ||||||
| 
 | 
 | ||||||
| export default class FeaturePipeline implements FeatureSource { | export default class FeaturePipeline implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|  | @ -22,10 +23,10 @@ export default class FeaturePipeline implements FeatureSource { | ||||||
|     public readonly name = "FeaturePipeline" |     public readonly name = "FeaturePipeline" | ||||||
| 
 | 
 | ||||||
|     constructor(flayers: UIEventSource<FilteredLayer[]>, |     constructor(flayers: UIEventSource<FilteredLayer[]>, | ||||||
|  |                 changes: Changes, | ||||||
|                 updater: FeatureSource, |                 updater: FeatureSource, | ||||||
|                 fromOsmApi: FeatureSource, |                 fromOsmApi: FeatureSource, | ||||||
|                 layout: UIEventSource<LayoutConfig>, |                 layout: UIEventSource<LayoutConfig>, | ||||||
|                 newPoints: FeatureSource, |  | ||||||
|                 locationControl: UIEventSource<Loc>, |                 locationControl: UIEventSource<Loc>, | ||||||
|                 selectedElement: UIEventSource<any>) { |                 selectedElement: UIEventSource<any>) { | ||||||
| 
 | 
 | ||||||
|  | @ -41,7 +42,9 @@ export default class FeaturePipeline implements FeatureSource { | ||||||
|                     new MetaTaggingFeatureSource(allLoadedFeatures, |                     new MetaTaggingFeatureSource(allLoadedFeatures, | ||||||
|                         new FeatureDuplicatorPerLayer(flayers, |                         new FeatureDuplicatorPerLayer(flayers, | ||||||
|                             new RegisteringFeatureSource( |                             new RegisteringFeatureSource( | ||||||
|                                 updater) |                                 new ChangeApplicator( | ||||||
|  |                                     updater, changes | ||||||
|  |                                 )) | ||||||
|                         )), layout)); |                         )), layout)); | ||||||
| 
 | 
 | ||||||
|         const geojsonSources: FeatureSource [] = GeoJsonSource |         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||||
|  | @ -49,8 +52,7 @@ export default class FeaturePipeline implements FeatureSource { | ||||||
|             .map(geojsonSource => { |             .map(geojsonSource => { | ||||||
|                 let source = new RegisteringFeatureSource( |                 let source = new RegisteringFeatureSource( | ||||||
|                     new FeatureDuplicatorPerLayer(flayers, |                     new FeatureDuplicatorPerLayer(flayers, | ||||||
|                             geojsonSource |                         new ChangeApplicator(geojsonSource, changes))); | ||||||
|                     )); |  | ||||||
|                 if (!geojsonSource.isOsmCache) { |                 if (!geojsonSource.isOsmCache) { | ||||||
|                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); |                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); | ||||||
|                 } |                 } | ||||||
|  | @ -58,25 +60,24 @@ export default class FeaturePipeline implements FeatureSource { | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|         const amendedLocalStorageSource = |         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( |         const amendedOsmApiSource = new RememberingSource( | ||||||
|             new MetaTaggingFeatureSource(allLoadedFeatures, |             new MetaTaggingFeatureSource(allLoadedFeatures, | ||||||
|                 new FeatureDuplicatorPerLayer(flayers, |                 new FeatureDuplicatorPerLayer(flayers, | ||||||
| 
 |                     new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes, | ||||||
|                     new RegisteringFeatureSource(fromOsmApi)))); |                         { | ||||||
|  |                             // We lump in the new points here
 | ||||||
|  |                             generateNewGeometries: true | ||||||
|  |                         } | ||||||
|  |                     ))))); | ||||||
| 
 | 
 | ||||||
|         const merged = |         const merged = | ||||||
|             new FeatureSourceMerger([ |             new FeatureSourceMerger([ | ||||||
|                 amendedOverpassSource, |                 amendedOverpassSource, | ||||||
|                 amendedOsmApiSource, |                 amendedOsmApiSource, | ||||||
|                 amendedLocalStorageSource, |                 amendedLocalStorageSource, | ||||||
|                 newPoints, |  | ||||||
|                 ...geojsonSources |                 ...geojsonSources | ||||||
|             ]); |             ]); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,6 +1,10 @@ | ||||||
| import FeatureSource from "./FeatureSource"; | import FeatureSource from "./FeatureSource"; | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * Merges features from different featureSources | ||||||
|  |  * Uses the freshest feature available in the case multiple sources offer data with the same identifier | ||||||
|  |  */ | ||||||
| export default class FeatureSourceMerger implements FeatureSource { | export default class FeatureSourceMerger implements FeatureSource { | ||||||
| 
 | 
 | ||||||
|     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); |     public features: UIEventSource<{ feature: any; freshness: Date }[]> = new UIEventSource<{ feature: any; freshness: Date }[]>([]); | ||||||
|  |  | ||||||
|  | @ -94,16 +94,9 @@ export default class FilteringFeatureSource implements FeatureSource { | ||||||
|                     return false; |                     return false; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                |  | ||||||
| 
 |  | ||||||
|                 return true; |                 return true; | ||||||
|             }); |             }); | ||||||
|             console.log( | 
 | ||||||
|                 "Filtering layer source: input: ", |  | ||||||
|                 upstream.features.data?.length, |  | ||||||
|                 "output:", |  | ||||||
|                 newFeatures.length |  | ||||||
|             ); |  | ||||||
|             self.features.setData(newFeatures); |             self.features.setData(newFeatures); | ||||||
|             if (missingLayers.size > 0) { |             if (missingLayers.size > 0) { | ||||||
|                 console.error( |                 console.error( | ||||||
|  |  | ||||||
|  | @ -15,15 +15,19 @@ export default class OsmApiFeatureSource implements FeatureSource { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     public load(id: string) { |     public load(id: string) { | ||||||
|         if(id.indexOf("-") >= 0){ |         if (id.indexOf("-") >= 0) { | ||||||
|             // Newly added point - not yet in OSM
 |             // Newly added point - not yet in OSM
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         console.debug("Downloading", id, "from the OSM-API") |         console.debug("Downloading", id, "from the OSM-API") | ||||||
|         OsmObject.DownloadObject(id).addCallbackAndRunD(element => { |         OsmObject.DownloadObject(id).addCallbackAndRunD(element => { | ||||||
|             const geojson = element.asGeoJson(); |             try { | ||||||
|             geojson.id = geojson.properties.id; |                 const geojson = element.asGeoJson(); | ||||||
|             this.features.setData([{feature: geojson, freshness: element.timestamp}]) |                 geojson.id = geojson.properties.id; | ||||||
|  |                 this.features.setData([{feature: geojson, freshness: element.timestamp}]) | ||||||
|  |             } catch (e) { | ||||||
|  |                 console.error(e) | ||||||
|  |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -58,7 +62,7 @@ export default class OsmApiFeatureSource implements FeatureSource { | ||||||
|             const bounds = Utils.tile_bounds(z, x, y); |             const bounds = Utils.tile_bounds(z, x, y); | ||||||
|             console.log("Loading OSM data tile", z, x, y, " with bounds", bounds) |             console.log("Loading OSM data tile", z, x, y, " with bounds", bounds) | ||||||
|             OsmObject.LoadArea(bounds, objects => { |             OsmObject.LoadArea(bounds, objects => { | ||||||
|                 const keptGeoJson: {feature:any, freshness: Date}[] = [] |                 const keptGeoJson: { feature: any, freshness: Date }[] = [] | ||||||
|                 // Which layer does the object match?
 |                 // Which layer does the object match?
 | ||||||
|                 for (const object of objects) { |                 for (const object of objects) { | ||||||
| 
 | 
 | ||||||
|  | @ -69,7 +73,7 @@ export default class OsmApiFeatureSource implements FeatureSource { | ||||||
|                         if (doesMatch) { |                         if (doesMatch) { | ||||||
|                             const geoJson = object.asGeoJson(); |                             const geoJson = object.asGeoJson(); | ||||||
|                             geoJson._matching_layer_id = layer.id |                             geoJson._matching_layer_id = layer.id | ||||||
|                             keptGeoJson.push({feature: geoJson, freshness:  object.timestamp}) |                             keptGeoJson.push({feature: geoJson, freshness: object.timestamp}) | ||||||
|                             break; |                             break; | ||||||
|                         } |                         } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										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()}; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     CreateChangeDescriptions(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 | ||||||
|  |         }] | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										48
									
								
								Logic/Osm/Actions/CreateNewNodeAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								Logic/Osm/Actions/CreateNewNodeAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | 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 extends OsmChangeAction { | ||||||
|  | 
 | ||||||
|  |     private readonly _basicTags: Tag[]; | ||||||
|  |     private readonly _lat: number; | ||||||
|  |     private readonly _lon: number; | ||||||
|  | 
 | ||||||
|  |     public newElementId : string = undefined | ||||||
|  |      | ||||||
|  |     constructor(basicTags: Tag[], lat: number, lon: number) { | ||||||
|  |         super() | ||||||
|  |         this._basicTags = basicTags; | ||||||
|  |         this._lat = lat; | ||||||
|  |         this._lon = lon; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     CreateChangeDescriptions(changes: Changes): ChangeDescription[] { | ||||||
|  |         const id = changes.getNewID() | ||||||
|  |         const properties = { | ||||||
|  |             id: "node/" + id | ||||||
|  |         } | ||||||
|  |         this.newElementId = "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 {UIEventSource} from "../../UIEventSource"; | ||||||
| import {Translation} from "../../UI/i18n/Translation"; | import {Translation} from "../../../UI/i18n/Translation"; | ||||||
| import Translations from "../../UI/i18n/Translations"; | import State from "../../../State"; | ||||||
| import {OsmObject} from "./OsmObject"; | import {OsmObject} from "../OsmObject"; | ||||||
| import State from "../../State"; | import Translations from "../../../UI/i18n/Translations"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../../Models/Constants"; | ||||||
| 
 | 
 | ||||||
| export default class DeleteAction { | export default class DeleteAction { | ||||||
| 
 | 
 | ||||||
|  | @ -30,7 +30,7 @@ export default class DeleteAction { | ||||||
|      * Does actually delete the feature; returns the event source 'this.isDeleted' |      * Does actually delete the feature; returns the event source 'this.isDeleted' | ||||||
|      * If deletion is not allowed, triggers the callback instead |      * 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 isDeleted = this.isDeleted | ||||||
|         const self = this; |         const self = this; | ||||||
|         let deletionStarted = false; |         let deletionStarted = false; | ||||||
|  | @ -75,8 +75,6 @@ export default class DeleteAction { | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
| 
 |  | ||||||
|         return isDeleted; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
							
								
								
									
										23
									
								
								Logic/Osm/Actions/OsmChangeAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								Logic/Osm/Actions/OsmChangeAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | ||||||
|  | /** | ||||||
|  |  * 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 { | ||||||
|  | 
 | ||||||
|  |     private isUsed = false | ||||||
|  | 
 | ||||||
|  |     public Perform(changes: Changes) { | ||||||
|  |         if (this.isUsed) { | ||||||
|  |             throw "This ChangeAction is already used: " + this.constructor.name | ||||||
|  |         } | ||||||
|  |         this.isUsed = true; | ||||||
|  |         return this.CreateChangeDescriptions(changes) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     protected abstract CreateChangeDescriptions(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() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     CreateChangeDescriptions(changes: Changes): ChangeDescription[] { | ||||||
|  |         return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										238
									
								
								Logic/Osm/Actions/SplitAction.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										238
									
								
								Logic/Osm/Actions/SplitAction.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,238 @@ | ||||||
|  | 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) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     CreateChangeDescriptions(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); | ||||||
|  |         // 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 | ||||||
|  |                     } | ||||||
|  |                     if (k.startsWith("_") || k === "id") { | ||||||
|  |                         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).CreateChangeDescriptions(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,233 @@ | ||||||
| import {OsmNode, OsmObject} from "./OsmObject"; | import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | ||||||
| import State from "../../State"; | import State from "../../State"; | ||||||
| import {Utils} from "../../Utils"; |  | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | import OsmChangeAction from "./Actions/OsmChangeAction"; | ||||||
| import {TagsFilter} from "../Tags/TagsFilter"; | import {ChangeDescription} from "./Actions/ChangeDescription"; | ||||||
| import {Tag} from "../Tags/Tag"; | import {Utils} from "../../Utils"; | ||||||
| import {OsmConnection} from "./OsmConnection"; |  | ||||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Handles all changes made to OSM. |  * Handles all changes made to OSM. | ||||||
|  * Needs an authenticator via OsmConnection |  * Needs an authenticator via OsmConnection | ||||||
|  */ |  */ | ||||||
| export class Changes implements FeatureSource { | export class Changes { | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 |     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||||
|     public readonly name = "Newly added features" |     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 }[]>([]); |     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 = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||||
|     private readonly isUploading = new UIEventSource(false); |     private readonly isUploading = new UIEventSource(false); | ||||||
|      |      | ||||||
|     /** |     private readonly previouslyCreated : OsmObject[] = [] | ||||||
|      * 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(" ")) { |     constructor() { | ||||||
|             console.warn("Tag starts with or ends with a space - trimming anyway") |  | ||||||
|         } |  | ||||||
|         |         | ||||||
|         return {k: key.trim(), v: value.trim()}; |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     private static createChangesetFor(csId: string, | ||||||
|  |                                      allChanges: { | ||||||
|  |                                          modifiedObjects: OsmObject[], | ||||||
|  |                                          newObjects: OsmObject[], | ||||||
|  |                                          deletedObjects: OsmObject[] | ||||||
|  |                                      }): string { | ||||||
| 
 | 
 | ||||||
|     addTag(elementId: string, tagsFilter: TagsFilter, |         const changedElements = allChanges.modifiedObjects ?? [] | ||||||
|            tags?: UIEventSource<any>) { |         const newElements = allChanges.newObjects ?? [] | ||||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); |         const deletedElements = allChanges.deletedObjects ?? [] | ||||||
|         const elementTags = eventSource.data; | 
 | ||||||
|         const changes = tagsFilter.asChange(elementTags).map(Changes.checkChange) |         let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`; | ||||||
|         if (changes.length == 0) { |         if (newElements.length > 0) { | ||||||
|             return; |             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 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") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         for (const o of this.previouslyCreated) { | ||||||
|  |             objects.set(o.type + "/" + o.id, o)  | ||||||
|  |             states.set(o.type + "/" + o.id, "unchanged") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let changed = false; | ||||||
|         for (const change of changes) { |         for (const change of changes) { | ||||||
|             if (elementTags[change.k] !== change.v) { |             const id = change.type + "/" + change.id | ||||||
|                 elementTags[change.k] = change.v; |             if (!objects.has(id)) { | ||||||
|                 console.log("Applied ", change.k, "=", change.v) |                 if(change.id >= 0){ | ||||||
|                 // We use 'elementTags.id' here, as we might have retrieved with the id 'node/-1' as new point, but should use the rewritten id
 |                     throw "Did not get an object that should be known: "+id | ||||||
|                 this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v}); |                 } | ||||||
|  |                 // This is a new object that should be created
 | ||||||
|  |                 states.set(id, "created") | ||||||
|  |                 console.log("Creating object for changeDescription", change) | ||||||
|  |                 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) | ||||||
|  |                 this.previouslyCreated.push(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,194 +235,65 @@ export class Changes implements FeatureSource { | ||||||
|      * Triggered by the 'PendingChangeUploader'-actor in Actors |      * Triggered by the 'PendingChangeUploader'-actor in Actors | ||||||
|      */ |      */ | ||||||
|     public flushChanges(flushreason: string = undefined) { |     public flushChanges(flushreason: string = undefined) { | ||||||
|         if (this.pending.data.length === 0) { |         if (this.pendingChanges.data.length === 0) { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         if (flushreason !== undefined) { |  | ||||||
|             console.log(flushreason) |  | ||||||
|         } |  | ||||||
|         this.uploadAll(); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     /** |  | ||||||
|      * 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 newId = Changes._nextId; |  | ||||||
|         Changes._nextId--; |  | ||||||
| 
 |  | ||||||
|         const id = "node/" + newId; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|         const properties = {id: id}; |  | ||||||
| 
 |  | ||||||
|         const geojson = { |  | ||||||
|             "type": "Feature", |  | ||||||
|             "properties": properties, |  | ||||||
|             "id": 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: 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: newId, 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; |  | ||||||
|         } |  | ||||||
|         const self = this; |  | ||||||
|          |          | ||||||
|         if (this.isUploading.data) { |         if (this.isUploading.data) { | ||||||
|  |             console.log("Is already uploading... Abort") | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  |          | ||||||
|  |        | ||||||
|         this.isUploading.setData(true) |         this.isUploading.setData(true) | ||||||
|         |         | ||||||
|         console.log("Beginning upload..."); |         console.log("Beginning upload... "+flushreason ?? ""); | ||||||
|         // At last, we build the changeset and upload
 |         // At last, we build the changeset and upload
 | ||||||
|         State.state.osmConnection.UploadChangeset( |  | ||||||
|             State.state.layoutToUse.data, |  | ||||||
|             State.state.allElements, |  | ||||||
|             function (csId) { |  | ||||||
| 
 |  | ||||||
|                 let modifications = ""; |  | ||||||
|                 for (const element of changedElements) { |  | ||||||
|                     if (!element.changed) { |  | ||||||
|                         continue; |  | ||||||
|                     } |  | ||||||
|                     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 += |  | ||||||
|                         "<create>" + |  | ||||||
|                         creations + |  | ||||||
|                         "</create>"; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 if (modifications.length > 0) { |  | ||||||
|                     changes += |  | ||||||
|                         "<modify>\n" + |  | ||||||
|                         modifications + |  | ||||||
|                         "\n</modify>"; |  | ||||||
|                 } |  | ||||||
| 
 |  | ||||||
|                 changes += "</osmChange>"; |  | ||||||
| 
 |  | ||||||
|                 return changes; |  | ||||||
|             }, |  | ||||||
|             () => { |  | ||||||
|                 console.log("Upload successfull!") |  | ||||||
|                 self.newObjects.setData([]) |  | ||||||
|                 self.pending.setData([]); |  | ||||||
|                 self.isUploading.setData(false) |  | ||||||
|             }, |  | ||||||
|             () => self.isUploading.setData(false) |  | ||||||
|         ); |  | ||||||
|     }; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     private uploadAll() { |  | ||||||
|         const self = this; |         const self = this; | ||||||
|  |         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: { | ||||||
|  |                 newObjects: OsmObject[], | ||||||
|  |                 modifiedObjects: OsmObject[] | ||||||
|  |                 deletedObjects: OsmObject[] | ||||||
| 
 | 
 | ||||||
|         const pending = this.pending.data; |             }  = self.CreateChangesetObjects(pending, osmObjects) | ||||||
|         let neededIds: string[] = []; |             if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { | ||||||
|         for (const change of pending) { |                 console.log("No changes to be made") | ||||||
|             const id = change.elementId; |                 self.pendingChanges.setData([]) | ||||||
|             if (parseFloat(id.split("/")[1]) < 0) { |                 self.isUploading.setData(false) | ||||||
|                 // New element - we don't have to download this
 |                 return true; // Unregister the callback
 | ||||||
|             } else { |  | ||||||
|                 neededIds.push(id); |  | ||||||
|             } |             } | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         neededIds = Utils.Dedup(neededIds); | 
 | ||||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { |             State.state.osmConnection.UploadChangeset( | ||||||
|             self.uploadChangesWithLatestVersions(knownElements) |                 State.state.layoutToUse.data, | ||||||
|         }) |                 State.state.allElements, | ||||||
|  |                 (csId) => Changes.createChangesetFor(csId, changes), | ||||||
|  |                 () => { | ||||||
|  |                     console.log("Upload successfull!") | ||||||
|  |                     self.pendingChanges.setData([]); | ||||||
|  |                     self.isUploading.setData(false) | ||||||
|  |                 }, | ||||||
|  |                 () => { | ||||||
|  |                     console.log("Upload failed - trying again later") | ||||||
|  |                     return self.isUploading.setData(false); | ||||||
|  |                 } // Failed - mark to try again
 | ||||||
|  |             ) | ||||||
|  |             return true; | ||||||
|  | 
 | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     public applyAction(action: OsmChangeAction) { | ||||||
|  |         const changes = action.Perform(this) | ||||||
|  |         console.log("Received changes:", changes) | ||||||
|  |         this.pendingChanges.data.push(...changes); | ||||||
|  |         this.pendingChanges.ping(); | ||||||
|  |     } | ||||||
| } | } | ||||||
|  | @ -53,6 +53,8 @@ export class ChangesetHandler { | ||||||
|                 element.ping(); |                 element.ping(); | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,7 +23,7 @@ export abstract class OsmObject { | ||||||
|         this.id = id; |         this.id = id; | ||||||
|         this.type = type; |         this.type = type; | ||||||
|         this.tags = { |         this.tags = { | ||||||
|             id: id |             id: `${this.type}/${id}` | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -51,7 +51,10 @@ export abstract class OsmObject { | ||||||
|         } |         } | ||||||
|         const splitted = id.split("/"); |         const splitted = id.split("/"); | ||||||
|         const type = splitted[0]; |         const type = splitted[0]; | ||||||
|         const idN = splitted[1]; |         const idN = Number(splitted[1]); | ||||||
|  |         if(idN <0){ | ||||||
|  |             return; | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|         OsmObject.objectCache.set(id, src); |         OsmObject.objectCache.set(id, src); | ||||||
|         const newContinuation = (element: OsmObject) => { |         const newContinuation = (element: OsmObject) => { | ||||||
|  | @ -68,6 +71,8 @@ export abstract class OsmObject { | ||||||
|             case("relation"): |             case("relation"): | ||||||
|                 new OsmRelation(idN).Download(newContinuation); |                 new OsmRelation(idN).Download(newContinuation); | ||||||
|                 break; |                 break; | ||||||
|  |             default: | ||||||
|  |                 throw "Invalid object type:" + type + id; | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|         return src; |         return src; | ||||||
|  | @ -103,7 +108,7 @@ export abstract class OsmObject { | ||||||
|         if (OsmObject.referencingRelationsCache.has(id)) { |         if (OsmObject.referencingRelationsCache.has(id)) { | ||||||
|             return OsmObject.referencingRelationsCache.get(id); |             return OsmObject.referencingRelationsCache.get(id); | ||||||
|         } |         } | ||||||
|         const relsSrc = new UIEventSource<OsmRelation[]>([]) |         const relsSrc = new UIEventSource<OsmRelation[]>(undefined) | ||||||
|         OsmObject.referencingRelationsCache.set(id, relsSrc); |         OsmObject.referencingRelationsCache.set(id, relsSrc); | ||||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`) |         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`) | ||||||
|             .then(data => { |             .then(data => { | ||||||
|  | @ -123,7 +128,7 @@ export abstract class OsmObject { | ||||||
|         } |         } | ||||||
|         const splitted = id.split("/"); |         const splitted = id.split("/"); | ||||||
|         const type = splitted[0]; |         const type = splitted[0]; | ||||||
|         const idN = splitted[1]; |         const idN = Number(splitted[1]); | ||||||
|         const src = new UIEventSource<OsmObject[]>([]); |         const src = new UIEventSource<OsmObject[]>([]); | ||||||
|         OsmObject.historyCache.set(id, src); |         OsmObject.historyCache.set(id, src); | ||||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => { |         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => { | ||||||
|  | @ -312,20 +317,6 @@ export abstract class OsmObject { | ||||||
|         return this; |         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; |     abstract ChangesetXML(changesetId: string): string; | ||||||
| 
 | 
 | ||||||
|  | @ -360,7 +351,7 @@ export class OsmNode extends OsmObject { | ||||||
|     lat: number; |     lat: number; | ||||||
|     lon: number; |     lon: number; | ||||||
| 
 | 
 | ||||||
|     constructor(id) { |     constructor(id: number) { | ||||||
|         super("node", id); |         super("node", id); | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
|  | @ -368,9 +359,9 @@ export class OsmNode extends OsmObject { | ||||||
|     ChangesetXML(changesetId: string): string { |     ChangesetXML(changesetId: string): string { | ||||||
|         let tags = this.TagsXML(); |         let tags = this.TagsXML(); | ||||||
| 
 | 
 | ||||||
|         return '        <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' + |         return '    <node id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + ' lat="' + this.lat + '" lon="' + this.lon + '">\n' + | ||||||
|             tags + |             tags + | ||||||
|             '        </node>\n'; |             '    </node>\n'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SaveExtraData(element) { |     SaveExtraData(element) { | ||||||
|  | @ -413,9 +404,8 @@ export class OsmWay extends OsmObject { | ||||||
|     lat: number; |     lat: number; | ||||||
|     lon: number; |     lon: number; | ||||||
| 
 | 
 | ||||||
|     constructor(id) { |     constructor(id: number) { | ||||||
|         super("way", id); |         super("way", id); | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|  | @ -432,7 +422,7 @@ export class OsmWay extends OsmObject { | ||||||
|         return '    <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' + |         return '    <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' + | ||||||
|             nds + |             nds + | ||||||
|             tags + |             tags + | ||||||
|             '        </way>\n'; |             '    </way>\n'; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     SaveExtraData(element, allNodes: OsmNode[]) { |     SaveExtraData(element, allNodes: OsmNode[]) { | ||||||
|  | @ -458,7 +448,7 @@ export class OsmWay extends OsmObject { | ||||||
|         this.nodes = element.nodes; |         this.nodes = element.nodes; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     asGeoJson() { |     public asGeoJson() { | ||||||
|         return { |         return { | ||||||
|             "type": "Feature", |             "type": "Feature", | ||||||
|             "properties": this.tags, |             "properties": this.tags, | ||||||
|  | @ -480,11 +470,14 @@ export class OsmWay extends OsmObject { | ||||||
| 
 | 
 | ||||||
| export class OsmRelation extends OsmObject { | export class OsmRelation extends OsmObject { | ||||||
| 
 | 
 | ||||||
|     members; |     public members: { | ||||||
|  |         type: "node" | "way" | "relation", | ||||||
|  |         ref: number, | ||||||
|  |         role: string | ||||||
|  |     }[]; | ||||||
| 
 | 
 | ||||||
|     constructor(id) { |     constructor(id: number) { | ||||||
|         super("relation", id); |         super("relation", id); | ||||||
| 
 |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     centerpoint(): [number, number] { |     centerpoint(): [number, number] { | ||||||
|  |  | ||||||
|  | @ -24,12 +24,19 @@ export class Tag extends TagsFilter { | ||||||
| 
 | 
 | ||||||
|     matchesProperties(properties: any): boolean { |     matchesProperties(properties: any): boolean { | ||||||
|         for (const propertiesKey in properties) { |         for (const propertiesKey in properties) { | ||||||
|  |             if(!properties.hasOwnProperty(propertiesKey)){ | ||||||
|  |                 continue | ||||||
|  |             } | ||||||
|             if (this.key === propertiesKey) { |             if (this.key === propertiesKey) { | ||||||
|                 const value = properties[propertiesKey]; |                 const value = properties[propertiesKey]; | ||||||
|  |                 if(value === undefined){ | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|                 return value === this.value; |                 return value === this.value; | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|         // The tag was not found
 |         // The tag was not found
 | ||||||
|  |          | ||||||
|         if (this.value === "") { |         if (this.value === "") { | ||||||
|             // and it shouldn't be found!
 |             // and it shouldn't be found!
 | ||||||
|             return true; |             return true; | ||||||
|  |  | ||||||
|  | @ -2,21 +2,27 @@ import {Utils} from "../Utils"; | ||||||
| 
 | 
 | ||||||
| export class UIEventSource<T> { | export class UIEventSource<T> { | ||||||
| 
 | 
 | ||||||
|  |     private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||||
|     public data: T; |     public data: T; | ||||||
|  |     public trace: boolean; | ||||||
|     private readonly tag: string; |     private readonly tag: string; | ||||||
|     private _callbacks = []; |     private _callbacks: ((t: T) => (boolean | void | any)) [] = []; | ||||||
| 
 | 
 | ||||||
|     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>[]{ |     static PrepPerf(): UIEventSource<any>[] { | ||||||
|         if(Utils.runningFromConsole){ |         if (Utils.runningFromConsole) { | ||||||
|             return []; |             return []; | ||||||
|         } |         } | ||||||
|         // @ts-ignore
 |         // @ts-ignore
 | ||||||
|         window.mapcomplete_performance = () => { |         window.mapcomplete_performance = () => { | ||||||
|             console.log(UIEventSource.allSources.length, "uieventsources created"); |             console.log(UIEventSource.allSources.length, "uieventsources created"); | ||||||
|             const copy = [...UIEventSource.allSources]; |             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:") |             console.log("Topten is:") | ||||||
|             for (let i = 0; i < 10; i++) { |             for (let i = 0; i < 10; i++) { | ||||||
|                 console.log(copy[i].tag, copy[i]); |                 console.log(copy[i].tag, copy[i]); | ||||||
|  | @ -26,12 +32,6 @@ export class UIEventSource<T> { | ||||||
|         return []; |         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> { |     public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> { | ||||||
|         const sink = new UIEventSource<X>(source.data?.data); |         const sink = new UIEventSource<X>(source.data?.data); | ||||||
| 
 | 
 | ||||||
|  | @ -63,11 +63,20 @@ export class UIEventSource<T> { | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public addCallback(callback: ((latestData: T) => void)): UIEventSource<T> { |     /** | ||||||
|  |      * Adds a callback | ||||||
|  |      * | ||||||
|  |      * If the result of the callback is 'true', the callback is considered finished and will be removed again | ||||||
|  |      * @param callback | ||||||
|  |      */ | ||||||
|  |     public addCallback(callback: ((latestData: T) => (boolean | void | any))): UIEventSource<T> { | ||||||
|         if (callback === console.log) { |         if (callback === console.log) { | ||||||
|             // This ^^^ actually works!
 |             // 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." |             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); |         this._callbacks.push(callback); | ||||||
|         return this; |         return this; | ||||||
|     } |     } | ||||||
|  | @ -87,8 +96,21 @@ export class UIEventSource<T> { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public ping(): void { |     public ping(): void { | ||||||
|  |         let toDelete = undefined | ||||||
|         for (const callback of this._callbacks) { |         for (const callback of this._callbacks) { | ||||||
|             callback(this.data); |             if (callback(this.data) === true) { | ||||||
|  |                 // This callback wants to be deleted
 | ||||||
|  |                 if (toDelete === undefined) { | ||||||
|  |                     toDelete = [callback] | ||||||
|  |                 } else { | ||||||
|  |                     toDelete.push(callback) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         if (toDelete !== undefined) { | ||||||
|  |             for (const toDeleteElement of toDelete) { | ||||||
|  |                 this._callbacks.splice(this._callbacks.indexOf(toDeleteElement), 1) | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  | @ -101,12 +123,12 @@ export class UIEventSource<T> { | ||||||
|      */ |      */ | ||||||
|     public map<J>(f: ((t: T) => J), |     public map<J>(f: ((t: T) => J), | ||||||
|                   extraSources: UIEventSource<any>[] = [], |                   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 self = this; | ||||||
| 
 | 
 | ||||||
|         const newSource = new UIEventSource<J>( |         const newSource = new UIEventSource<J>( | ||||||
|             f(this.data), |             f(this.data), | ||||||
|             "map("+this.tag+")" |             "map(" + this.tag + ")" | ||||||
|         ); |         ); | ||||||
| 
 | 
 | ||||||
|         const update = function () { |         const update = function () { | ||||||
|  | @ -159,10 +181,10 @@ export class UIEventSource<T> { | ||||||
|         return newSource; |         return newSource; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     addCallbackAndRunD(callback: (data :T ) => void) { |     addCallbackAndRunD(callback: (data: T) => void) { | ||||||
|         this.addCallbackAndRun(data => { |         this.addCallbackAndRun(data => { | ||||||
|             if(data !== undefined && data !== null){ |             if (data !== undefined && data !== null) { | ||||||
|                 callback(data) |               return  callback(data) | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import { Utils } from "../Utils"; | ||||||
| 
 | 
 | ||||||
| export default class Constants { | export default class Constants { | ||||||
|      |      | ||||||
|     public static vNumber = "0.8.4a"; |     public static vNumber = "0.9.0-rc0"; | ||||||
| 
 | 
 | ||||||
|     // The user journey states thresholds when a new feature gets unlocked
 |     // The user journey states thresholds when a new feature gets unlocked
 | ||||||
|     public static userJourney = { |     public static userJourney = { | ||||||
|  |  | ||||||
							
								
								
									
										22
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -14,14 +14,13 @@ import Loc from "./Models/Loc"; | ||||||
| import Constants from "./Models/Constants"; | import Constants from "./Models/Constants"; | ||||||
| 
 | 
 | ||||||
| import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; | import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; | ||||||
| import LayerConfig from "./Customizations/JSON/LayerConfig"; |  | ||||||
| import TitleHandler from "./Logic/Actors/TitleHandler"; | import TitleHandler from "./Logic/Actors/TitleHandler"; | ||||||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||||
| import {Relation} from "./Logic/Osm/ExtractRelations"; | import {Relation} from "./Logic/Osm/ExtractRelations"; | ||||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | ||||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||||
| import { TagsFilter } from "./Logic/Tags/TagsFilter"; |  | ||||||
| import FilteredLayer from "./Models/FilteredLayer"; | import FilteredLayer from "./Models/FilteredLayer"; | ||||||
|  | import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Contains the global state: a bunch of UI-event sources |  * Contains the global state: a bunch of UI-event sources | ||||||
|  | @ -31,7 +30,7 @@ export default class State { | ||||||
|     // The singleton of the global state
 |     // The singleton of the global state
 | ||||||
|     public static state: State; |     public static state: State; | ||||||
| 
 | 
 | ||||||
|     public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined); |     public readonly layoutToUse = new UIEventSource<LayoutConfig>(undefined, "layoutToUse"); | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      The mapping from id -> UIEventSource<properties> |      The mapping from id -> UIEventSource<properties> | ||||||
|  | @ -44,7 +43,7 @@ export default class State { | ||||||
|     /** |     /** | ||||||
|      The leaflet instance of the big basemap |      The leaflet instance of the big basemap | ||||||
|      */ |      */ | ||||||
|     public leafletMap = new UIEventSource<L.Map>(undefined); |     public leafletMap = new UIEventSource<L.Map>(undefined, "leafletmap"); | ||||||
|     /** |     /** | ||||||
|      * Background layer id |      * Background layer id | ||||||
|      */ |      */ | ||||||
|  | @ -62,7 +61,7 @@ export default class State { | ||||||
| 
 | 
 | ||||||
|     public osmApiFeatureSource: OsmApiFeatureSource; |     public osmApiFeatureSource: OsmApiFeatureSource; | ||||||
| 
 | 
 | ||||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([]); |     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([],"filteredLayers"); | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      The latest element that was selected |      The latest element that was selected | ||||||
|  | @ -102,7 +101,7 @@ export default class State { | ||||||
|     /** |     /** | ||||||
|      * The map location: currently centered lat, lon and zoom |      * The map location: currently centered lat, lon and zoom | ||||||
|      */ |      */ | ||||||
|     public readonly locationControl = new UIEventSource<Loc>(undefined); |     public readonly locationControl = new UIEventSource<Loc>(undefined, "locationControl"); | ||||||
|     public backgroundLayer; |     public backgroundLayer; | ||||||
|     public readonly backgroundLayerId: UIEventSource<string>; |     public readonly backgroundLayerId: UIEventSource<string>; | ||||||
| 
 | 
 | ||||||
|  | @ -187,11 +186,13 @@ export default class State { | ||||||
|                 ).syncWith(LocalStorageSource.Get("lon")) |                 ).syncWith(LocalStorageSource.Get("lon")) | ||||||
|             ); |             ); | ||||||
| 
 | 
 | ||||||
|             this.locationControl = new UIEventSource<Loc>({ |             this.locationControl.setData({ | ||||||
|                 zoom: Utils.asFloat(zoom.data), |                 zoom: Utils.asFloat(zoom.data), | ||||||
|                 lat: Utils.asFloat(lat.data), |                 lat: Utils.asFloat(lat.data), | ||||||
|                 lon: Utils.asFloat(lon.data), |                 lon: Utils.asFloat(lon.data), | ||||||
|             }).addCallback((latlonz) => { |             }) | ||||||
|  |             this.locationControl.addCallback((latlonz) => { | ||||||
|  |                 // Sync th location controls
 | ||||||
|                 zoom.setData(latlonz.zoom); |                 zoom.setData(latlonz.zoom); | ||||||
|                 lat.setData(latlonz.lat); |                 lat.setData(latlonz.lat); | ||||||
|                 lon.setData(latlonz.lon); |                 lon.setData(latlonz.lon); | ||||||
|  | @ -371,7 +372,10 @@ export default class State { | ||||||
| 
 | 
 | ||||||
|         this.allElements = new ElementStorage(); |         this.allElements = new ElementStorage(); | ||||||
|         this.changes = new Changes(); |         this.changes = new Changes(); | ||||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource(); |          | ||||||
|  |         new ChangeToElementsActor(this.changes, this.allElements) | ||||||
|  |          | ||||||
|  |         this.osmApiFeatureSource = new OsmApiFeatureSource() | ||||||
| 
 | 
 | ||||||
|         new PendingChangesUploader(this.changes, this.selectedElement); |         new PendingChangesUploader(this.changes, this.selectedElement); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										417
									
								
								Svg.ts
									
										
									
									
									
								
							
							
						
						
									
										417
									
								
								Svg.ts
									
										
									
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							|  | @ -28,7 +28,7 @@ export default class Minimap extends BaseUIElement { | ||||||
|         super() |         super() | ||||||
|         options = options ?? {} |         options = options ?? {} | ||||||
|         this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) |         this._background = options?.background ?? new UIEventSource<BaseLayer>(AvailableBaseLayers.osmCarto) | ||||||
|         this._location = options?.location ?? new UIEventSource<Loc>(undefined) |         this._location = options?.location ?? new UIEventSource<Loc>({lat: 0, lon: 0, zoom: 1}) | ||||||
|         this._id = "minimap" + Minimap._nextId; |         this._id = "minimap" + Minimap._nextId; | ||||||
|         this._allowMoving = options.allowMoving ?? true; |         this._allowMoving = options.allowMoving ?? true; | ||||||
|         this._leafletoptions = options.leafletOptions ?? {} |         this._leafletoptions = options.leafletOptions ?? {} | ||||||
|  | @ -43,6 +43,7 @@ export default class Minimap extends BaseUIElement { | ||||||
|         div.style.width = "100%" |         div.style.width = "100%" | ||||||
|         div.style.minWidth = "40px" |         div.style.minWidth = "40px" | ||||||
|         div.style.minHeight = "40px" |         div.style.minHeight = "40px" | ||||||
|  |         div.style.position = "relative" | ||||||
|         const wrapper = document.createElement("div") |         const wrapper = document.createElement("div") | ||||||
|         wrapper.appendChild(div) |         wrapper.appendChild(div) | ||||||
|         const self = this; |         const self = this; | ||||||
|  |  | ||||||
|  | @ -64,13 +64,11 @@ export default class PersonalLayersPanel extends VariableUiElement { | ||||||
|     private static CreateLayerToggle(layer: LayerConfig): Toggle { |     private static CreateLayerToggle(layer: LayerConfig): Toggle { | ||||||
|         let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle( |         let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle( | ||||||
|             new UIEventSource<any>({id: "node/-1"}), |             new UIEventSource<any>({id: "node/-1"}), | ||||||
|             false, |             false | ||||||
|             "2em" |  | ||||||
|         ).icon.html]).SetClass("relative") |         ).icon.html]).SetClass("relative") | ||||||
|         let iconUnset =new Combine([ layer.GenerateLeafletStyle( |         let iconUnset =new Combine([ layer.GenerateLeafletStyle( | ||||||
|             new UIEventSource<any>({id: "node/-1"}), |             new UIEventSource<any>({id: "node/-1"}), | ||||||
|             false, |             false | ||||||
|             "2em" |  | ||||||
|         ).icon.html]).SetClass("relative") |         ).icon.html]).SetClass("relative") | ||||||
| 
 | 
 | ||||||
|         iconUnset.SetStyle("opacity:0.1") |         iconUnset.SetStyle("opacity:0.1") | ||||||
|  |  | ||||||
|  | @ -20,6 +20,8 @@ import LocationInput from "../Input/LocationInput"; | ||||||
| import {InputElement} from "../Input/InputElement"; | import {InputElement} from "../Input/InputElement"; | ||||||
| import Loc from "../../Models/Loc"; | import Loc from "../../Models/Loc"; | ||||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||||
|  | import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||||
|  | import Hash from "../../Logic/Web/Hash"; | ||||||
| 
 | 
 | ||||||
| /* | /* | ||||||
| * The SimpleAddUI is a single panel, which can have multiple states: | * The SimpleAddUI is a single panel, which can have multiple states: | ||||||
|  | @ -61,11 +63,6 @@ export default class SimpleAddUI extends Toggle { | ||||||
|         const selectedPreset = new UIEventSource<PresetInfo>(undefined); |         const selectedPreset = new UIEventSource<PresetInfo>(undefined); | ||||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 |         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 presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) | ||||||
| 
 | 
 | ||||||
|         const addUi = new VariableUiElement( |         const addUi = new VariableUiElement( | ||||||
|  | @ -75,8 +72,16 @@ export default class SimpleAddUI extends Toggle { | ||||||
|                     } |                     } | ||||||
|                     return SimpleAddUI.CreateConfirmButton(preset, |                     return SimpleAddUI.CreateConfirmButton(preset, | ||||||
|                         (tags, location) => { |                         (tags, location) => { | ||||||
|                             createNewPoint(tags, location) |                         const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon) | ||||||
|  |                             State.state.changes.applyAction(newElementAction) | ||||||
|                             selectedPreset.setData(undefined) |                             selectedPreset.setData(undefined) | ||||||
|  |                             isShown.setData(false) | ||||||
|  |                             State.state.selectedElement.setData(State.state.allElements.ContainingFeatures.get( | ||||||
|  |                                 newElementAction.newElementId | ||||||
|  |                             )) | ||||||
|  |                             console.log("Did set selected element to",State.state.allElements.ContainingFeatures.get( | ||||||
|  |                                 newElementAction.newElementId | ||||||
|  |                             )) | ||||||
|                         }, () => { |                         }, () => { | ||||||
|                             selectedPreset.setData(undefined) |                             selectedPreset.setData(undefined) | ||||||
|                         }) |                         }) | ||||||
|  | @ -123,13 +128,13 @@ export default class SimpleAddUI extends Toggle { | ||||||
|             }); |             }); | ||||||
| 
 | 
 | ||||||
|             let backgroundLayer = undefined; |             let backgroundLayer = undefined; | ||||||
|             if(preset.preciseInput.preferredBackground){ |             if (preset.preciseInput.preferredBackground) { | ||||||
|                backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) |                 backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             preciseInput = new LocationInput({ |             preciseInput = new LocationInput({ | ||||||
|                 mapBackground: backgroundLayer, |                 mapBackground: backgroundLayer, | ||||||
|                 centerLocation:locationSrc |                 centerLocation: locationSrc | ||||||
| 
 | 
 | ||||||
|             }) |             }) | ||||||
|             preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") |             preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") | ||||||
|  | @ -241,7 +246,7 @@ export default class SimpleAddUI extends Toggle { | ||||||
|             for (const preset of presets) { |             for (const preset of presets) { | ||||||
| 
 | 
 | ||||||
|                 const tags = TagUtils.KVtoProperties(preset.tags ?? []); |                 const tags = TagUtils.KVtoProperties(preset.tags ?? []); | ||||||
|                 let icon:() => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html |                 let icon: () => BaseUIElement = () => layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>(tags), false).icon.html | ||||||
|                     .SetClass("w-12 h-12 block relative"); |                     .SetClass("w-12 h-12 block relative"); | ||||||
|                 const presetInfo: PresetInfo = { |                 const presetInfo: PresetInfo = { | ||||||
|                     tags: preset.tags, |                     tags: preset.tags, | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import Combine from "../Base/Combine"; | ||||||
| import State from "../../State"; | import State from "../../State"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import {Tag} from "../../Logic/Tags/Tag"; | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
|  | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| export default class DeleteImage extends Toggle { | export default class DeleteImage extends Toggle { | ||||||
|  | @ -15,14 +16,17 @@ export default class DeleteImage extends Toggle { | ||||||
|             .SetClass("rounded-full p-1") |             .SetClass("rounded-full p-1") | ||||||
|             .SetStyle("color:white;background:#ff8c8c") |             .SetStyle("color:white;background:#ff8c8c") | ||||||
|             .onClick(() => { |             .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() |         const deleteButton = Translations.t.image.doDelete.Clone() | ||||||
|             .SetClass("block w-full pl-4 pr-4") |             .SetClass("block w-full pl-4 pr-4") | ||||||
|             .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;") |             .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;") | ||||||
|             .onClick(() => { |             .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;"); |         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 ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; | ||||||
| import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; | import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; | ||||||
| import LayerConfig from "../../Customizations/JSON/LayerConfig"; | import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||||
|  | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||||
| 
 | 
 | ||||||
| export class ImageUploadFlow extends Toggle { | export class ImageUploadFlow extends Toggle { | ||||||
| 
 | 
 | ||||||
|  | @ -28,7 +29,10 @@ export class ImageUploadFlow extends Toggle { | ||||||
|                 key = imagePrefix + ":" + freeIndex; |                 key = imagePrefix + ":" + freeIndex; | ||||||
|             } |             } | ||||||
|             console.log("Adding image:" + key, url); |             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 | ||||||
|  |                 )) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -42,7 +42,6 @@ export default class LocationInput extends InputElement<Loc> { | ||||||
|             } |             } | ||||||
|         ) |         ) | ||||||
|         map.leafletMap.addCallbackAndRunD(leaflet => { |         map.leafletMap.addCallbackAndRunD(leaflet => { | ||||||
|             console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15)) |  | ||||||
|             leaflet.setMaxBounds( |             leaflet.setMaxBounds( | ||||||
|                 leaflet.getBounds().pad(0.15) |                 leaflet.getBounds().pad(0.15) | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  | @ -3,7 +3,7 @@ import State from "../../State"; | ||||||
| import Toggle from "../Input/Toggle"; | import Toggle from "../Input/Toggle"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
| import Svg from "../../Svg"; | import Svg from "../../Svg"; | ||||||
| import DeleteAction from "../../Logic/Osm/DeleteAction"; | import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; | ||||||
| import {Tag} from "../../Logic/Tags/Tag"; | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||||
|  | @ -19,6 +19,7 @@ import {Changes} from "../../Logic/Osm/Changes"; | ||||||
| import {And} from "../../Logic/Tags/And"; | import {And} from "../../Logic/Tags/And"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import DeleteConfig from "../../Customizations/JSON/DeleteConfig"; | import DeleteConfig from "../../Customizations/JSON/DeleteConfig"; | ||||||
|  | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||||
| 
 | 
 | ||||||
| export default class DeleteWizard extends Toggle { | export default class DeleteWizard extends Toggle { | ||||||
|     /** |     /** | ||||||
|  | @ -58,7 +59,9 @@ export default class DeleteWizard extends Toggle { | ||||||
|                 }) |                 }) | ||||||
|             } |             } | ||||||
|             (State.state?.changes ?? new Changes()) |             (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) { |         function doDelete(selected: TagsFilter) { | ||||||
|  |  | ||||||
|  | @ -13,6 +13,7 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; | ||||||
| import BaseUIElement from "../BaseUIElement"; | import BaseUIElement from "../BaseUIElement"; | ||||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import DeleteWizard from "./DeleteWizard"; | import DeleteWizard from "./DeleteWizard"; | ||||||
|  | import SplitRoadWizard from "./SplitRoadWizard"; | ||||||
| 
 | 
 | ||||||
| export default class FeatureInfoBox extends ScrollableFullScreen { | export default class FeatureInfoBox extends ScrollableFullScreen { | ||||||
| 
 | 
 | ||||||
|  | @ -66,10 +67,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | ||||||
|             renderings.push(questionBox); |             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) { |         if (layerConfig.deletion) { | ||||||
|             renderings.push( |             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( |         renderings.push( | ||||||
|             new VariableUiElement( |             new VariableUiElement( | ||||||
|                 State.state.osmConnection.userDetails |                 State.state.osmConnection.userDetails | ||||||
|  |  | ||||||
							
								
								
									
										155
									
								
								UI/Popup/SplitRoadWizard.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								UI/Popup/SplitRoadWizard.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,155 @@ | ||||||
|  | import Toggle from "../Input/Toggle"; | ||||||
|  | import Svg from "../../Svg"; | ||||||
|  | import {UIEventSource} from "../../Logic/UIEventSource"; | ||||||
|  | import {SubtleButton} from "../Base/SubtleButton"; | ||||||
|  | import Minimap from "../Base/Minimap"; | ||||||
|  | import State from "../../State"; | ||||||
|  | import ShowDataLayer from "../ShowDataLayer"; | ||||||
|  | import {GeoOperations} from "../../Logic/GeoOperations"; | ||||||
|  | import {LeafletMouseEvent} from "leaflet"; | ||||||
|  | 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/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()) | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * A UI Element used for splitting roads | ||||||
|  |      * | ||||||
|  |      * @param id: The id of the road to remove | ||||||
|  |      */ | ||||||
|  |     constructor(id: string) { | ||||||
|  | 
 | ||||||
|  |         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 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, 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 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, "splitRoadWay"); | ||||||
|  |         new ShowDataLayer(splitPoints, miniMap.leafletMap, SplitRoadWizard.splitLayout, false, false, "splitRoad: splitpoints") | ||||||
|  | 
 | ||||||
|  |         /** | ||||||
|  |          * Handles a click on the overleaf map. | ||||||
|  |          * Finds the closest intersection with the road and adds a point there, ready to confirm the cut. | ||||||
|  |          * @param coordinates Clicked location, [lon, lat] | ||||||
|  |          */ | ||||||
|  |         function onMapClick(coordinates) { | ||||||
|  |             // Get nearest point on the road
 | ||||||
|  |             const pointOnRoad = GeoOperations.nearestPoint(roadElement, coordinates); // pointOnRoad is a geojson
 | ||||||
|  | 
 | ||||||
|  |             // Update point properties to let it match the layer
 | ||||||
|  |             pointOnRoad.properties._cutposition = "yes"; | ||||||
|  |             pointOnRoad["_matching_layer_id"] = "splitpositions"; | ||||||
|  | 
 | ||||||
|  |             // let the state remember the point, to be able to retrieve it later by id
 | ||||||
|  |             State.state.allElements.addOrGetElement(pointOnRoad); | ||||||
|  | 
 | ||||||
|  |             // Add it to the list of all points and notify observers
 | ||||||
|  |             splitPoints.data.push({feature: pointOnRoad, freshness: new Date()}); // show the point on the data layer
 | ||||||
|  |             splitPoints.ping(); // not updated using .setData, so manually ping observers
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // When clicked, pass clicked location coordinates to onMapClick function
 | ||||||
|  |         miniMap.leafletMap.addCallbackAndRunD( | ||||||
|  |             (leafletMap) => leafletMap.on("click", (mouseEvent: LeafletMouseEvent) => { | ||||||
|  |                 onMapClick([mouseEvent.latlng.lng, mouseEvent.latlng.lat]) | ||||||
|  |             })) | ||||||
|  | 
 | ||||||
|  |         // Toggle between splitmap
 | ||||||
|  |         const splitButton = new SubtleButton(Svg.scissors_ui(), t.inviteToSplit.Clone()); | ||||||
|  |         splitButton.onClick( | ||||||
|  |             () => { | ||||||
|  |                 splitClicked.setData(true) | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         // Only show the splitButton if logged in, else show login prompt
 | ||||||
|  |         const loginBtn = t.loginToSplit.Clone() | ||||||
|  |             .onClick(() => State.state.osmConnection.AttemptLogin()) | ||||||
|  |             .SetClass("login-button-friendly"); | ||||||
|  |         const splitToggle = new Toggle(splitButton, loginBtn, State.state.osmConnection.isLoggedIn) | ||||||
|  | 
 | ||||||
|  |         // Save button
 | ||||||
|  |         const saveButton = new Button(t.split.Clone(), () => { | ||||||
|  |             hasBeenSplit.setData(true) | ||||||
|  |             const way = OsmObject.DownloadObject(id) | ||||||
|  |             const partOfSrc = OsmObject.DownloadReferencingRelations(id); | ||||||
|  |             let hasRun = false | ||||||
|  |             way.map(way => { | ||||||
|  |                 const partOf = partOfSrc.data | ||||||
|  |                 if(way === undefined || partOf === undefined){ | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if(hasRun){ | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |                 hasRun = true | ||||||
|  |                 const splitAction = new SplitAction( | ||||||
|  |                     <OsmWay>way, way.asGeoJson(), partOf, splitPoints.data.map(ff => ff.feature) | ||||||
|  |                 ) | ||||||
|  |                 State.state.changes.applyAction(splitAction) | ||||||
|  |                  | ||||||
|  |             }, [partOfSrc]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         }); | ||||||
|  |         saveButton.SetClass("btn btn-primary mr-3"); | ||||||
|  |         const disabledSaveButton = new Button("Split", undefined); | ||||||
|  |         disabledSaveButton.SetClass("btn btn-disabled mr-3"); | ||||||
|  |         // 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 = Translations.t.general.cancel.Clone() // Not using Button() element to prevent full width button
 | ||||||
|  |             .SetClass("btn btn-secondary mr-3") | ||||||
|  |             .onClick(() => { | ||||||
|  |                 splitPoints.setData([]); | ||||||
|  |                 splitClicked.setData(false); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |         cancelButton.SetClass("btn btn-secondary block"); | ||||||
|  | 
 | ||||||
|  |         const splitTitle = new Title(t.splitTitle); | ||||||
|  | 
 | ||||||
|  |         const mapView = new Combine([splitTitle, miniMap, new Combine([cancelButton, saveToggle]).SetClass("flex flex-row")]); | ||||||
|  |         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: ["en"], | ||||||
|  |             startLon: 0, | ||||||
|  |             startLat: 0, | ||||||
|  |             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, "(BUILTIN) SplitRoadWizard.ts") | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | @ -25,6 +25,7 @@ import BaseUIElement from "../BaseUIElement"; | ||||||
| import {DropDown} from "../Input/DropDown"; | import {DropDown} from "../Input/DropDown"; | ||||||
| import {Unit} from "../../Customizations/JSON/Denomination"; | import {Unit} from "../../Customizations/JSON/Denomination"; | ||||||
| import InputElementWrapper from "../Input/InputElementWrapper"; | import InputElementWrapper from "../Input/InputElementWrapper"; | ||||||
|  | import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Shows the question element. |  * Shows the question element. | ||||||
|  | @ -56,7 +57,9 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|             const selection = inputElement.GetValue().data; |             const selection = inputElement.GetValue().data; | ||||||
|             if (selection) { |             if (selection) { | ||||||
|                 (State.state?.changes ?? new Changes()) |                 (State.state?.changes ?? new Changes()) | ||||||
|                     .addTag(tags.data.id, selection, tags); |                     .applyAction(new ChangeTagAction( | ||||||
|  |                         tags.data.id, selection, tags.data | ||||||
|  |                     )) | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (options.afterSave) { |             if (options.afterSave) { | ||||||
|  | @ -195,9 +198,7 @@ export default class TagRenderingQuestion extends Combine { | ||||||
|                     oppositeTags.push(notSelected); |                     oppositeTags.push(notSelected); | ||||||
|                 } |                 } | ||||||
|                 tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); |                 tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); | ||||||
|                 const actualTags = TagUtils.FlattenMultiAnswer(tags); |                 return TagUtils.FlattenMultiAnswer(tags); | ||||||
|                 console.log("Converted ", indices.join(","), "into", actualTags.asHumanString(false, false, {}), "with elems", elements) |  | ||||||
|                 return actualTags; |  | ||||||
|             }, |             }, | ||||||
|             (tags: TagsFilter) => { |             (tags: TagsFilter) => { | ||||||
|                 // {key --> values[]}
 |                 // {key --> values[]}
 | ||||||
|  |  | ||||||
|  | @ -22,7 +22,8 @@ export default class ShowDataLayer { | ||||||
|                 leafletMap: UIEventSource<L.Map>, |                 leafletMap: UIEventSource<L.Map>, | ||||||
|                 layoutToUse: UIEventSource<LayoutConfig>, |                 layoutToUse: UIEventSource<LayoutConfig>, | ||||||
|                 enablePopups = true, |                 enablePopups = true, | ||||||
|                 zoomToFeatures = false) { |                 zoomToFeatures = false, | ||||||
|  |                 name?: string) { | ||||||
|         this._leafletMap = leafletMap; |         this._leafletMap = leafletMap; | ||||||
|         this._enablePopups = enablePopups; |         this._enablePopups = enablePopups; | ||||||
|         this._features = features; |         this._features = features; | ||||||
|  | @ -85,9 +86,7 @@ export default class ShowDataLayer { | ||||||
|                     console.error(e) |                     console.error(e) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
| 
 |             State.state.selectedElement.ping() | ||||||
| 
 |  | ||||||
|             State.state.selectedElement.ping(); |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         features.addCallback(() => update()); |         features.addCallback(() => update()); | ||||||
|  | @ -131,6 +130,7 @@ export default class ShowDataLayer { | ||||||
|             }) |             }) | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
|     private postProcessFeature(feature, leafletLayer: L.Layer) { |     private postProcessFeature(feature, leafletLayer: L.Layer) { | ||||||
|         const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; |         const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; | ||||||
|         if (layer === undefined) { |         if (layer === undefined) { | ||||||
|  | @ -188,11 +188,13 @@ export default class ShowDataLayer { | ||||||
|             if (selected.properties.id === feature.properties.id) { |             if (selected.properties.id === feature.properties.id) { | ||||||
|                 // A small sanity check to prevent infinite loops:
 |                 // A small sanity check to prevent infinite loops:
 | ||||||
|                 if (selected.geometry.type === feature.geometry.type  // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
 |                 if (selected.geometry.type === feature.geometry.type  // If a feature is rendered both as way and as point, opening one popup might trigger the other to open, which might trigger the one to open again
 | ||||||
| 
 |                     && feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
 | ||||||
|                     &&                     feature.id === feature.properties.id // the feature might have as id 'node/-1' and as 'feature.properties.id' = 'the newly assigned id'. That is no good too
 |                 ) { | ||||||
|                      ) { |  | ||||||
|                     leafletLayer.openPopup() |                     leafletLayer.openPopup() | ||||||
|                 } |                 } | ||||||
|  |                 if(feature.id !== feature.properties.id){ | ||||||
|  |                     console.trace("Not opening the popup for", feature) | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|             } |             } | ||||||
|         }) |         }) | ||||||
|  |  | ||||||
|  | @ -56,9 +56,12 @@ export default class SpecialVisualizations { | ||||||
|                             if (!tags.hasOwnProperty(key)) { |                             if (!tags.hasOwnProperty(key)) { | ||||||
|                                 continue; |                                 continue; | ||||||
|                             } |                             } | ||||||
|                             parts.push(key + "=" + tags[key]); |                             parts.push([key , tags[key] ?? "<b>undefined</b>" ]); | ||||||
|                         } |                         } | ||||||
|                         return parts.join("<br/>") |                         return new Table( | ||||||
|  |                             ["key","value"], | ||||||
|  |                             parts | ||||||
|  |                         ) | ||||||
|                     })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") |                     })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") | ||||||
|                 }) |                 }) | ||||||
|             }, |             }, | ||||||
|  | @ -127,6 +130,7 @@ export default class SpecialVisualizations { | ||||||
|                                 // This is a list of values
 |                                 // This is a list of values
 | ||||||
|                                 idList = JSON.parse(value) |                                 idList = JSON.parse(value) | ||||||
|                             } |                             } | ||||||
|  |                              | ||||||
|                             for (const id of idList) { |                             for (const id of idList) { | ||||||
|                                 features.push({ |                                 features.push({ | ||||||
|                                     freshness: new Date(), |                                     freshness: new Date(), | ||||||
|  |  | ||||||
|  | @ -19,7 +19,6 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|         const extraMappings: SpecialVisualization[] = []; |         const extraMappings: SpecialVisualization[] = []; | ||||||
| 
 | 
 | ||||||
|         mapping?.forEach((value, key) => { |         mapping?.forEach((value, key) => { | ||||||
|             console.log("KV:", key, value) |  | ||||||
|             extraMappings.push( |             extraMappings.push( | ||||||
|                 { |                 { | ||||||
|                     funcName: key, |                     funcName: key, | ||||||
|  | @ -73,11 +72,6 @@ export class SubstitutedTranslation extends VariableUiElement { | ||||||
|         } |         } | ||||||
|     }[] { |     }[] { | ||||||
| 
 | 
 | ||||||
|         if (extraMappings.length > 0) { |  | ||||||
| 
 |  | ||||||
|             console.log("Extra mappings are", extraMappings) |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|         for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) { |         for (const knownSpecial of SpecialVisualizations.specialVisualizations.concat(extraMappings)) { | ||||||
| 
 | 
 | ||||||
|             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 |             // Note: the '.*?' in the regex reads as 'any character, but in a non-greedy way'
 | ||||||
|  |  | ||||||
|  | @ -109,9 +109,9 @@ export class Translation extends BaseUIElement { | ||||||
|                     // @ts-ignore
 |                     // @ts-ignore
 | ||||||
|                     const date: Date = el; |                     const date: Date = el; | ||||||
|                     rtext = date.toLocaleString(); |                     rtext = date.toLocaleString(); | ||||||
|                 } else if (el.ConstructElement() === undefined) { |                 } else if (el.ConstructElement === undefined) { | ||||||
|                     console.error("InnerREnder is not defined", el); |                     console.error("ConstructElement is not defined", el); | ||||||
|                     throw "Hmmm, el.InnerRender is not defined?" |                     throw "ConstructElement is not defined, you are working with a "+(typeof el)+":"+(el.constructor.name) | ||||||
|                 } else { |                 } else { | ||||||
|                     Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
 |                     Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
 | ||||||
|                     rtext = el.ConstructElement().innerHTML; |                     rtext = el.ConstructElement().innerHTML; | ||||||
|  |  | ||||||
							
								
								
									
										13
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										13
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -136,6 +136,19 @@ export class Utils { | ||||||
|         return newArr; |         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) { |     public static MergeTags(a: any, b: any) { | ||||||
|         const t = {}; |         const t = {}; | ||||||
|         for (const k in a) { |         for (const k in a) { | ||||||
|  |  | ||||||
|  | @ -1302,5 +1302,15 @@ | ||||||
|     "sources": [ |     "sources": [ | ||||||
|       "https://commons.wikimedia.org/wiki/File:Media-floppy.svg" |       "https://commons.wikimedia.org/wiki/File:Media-floppy.svg" | ||||||
|     ] |     ] | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     "authors": [ | ||||||
|  |       "The noun project - Basith Ibrahi" | ||||||
|  |     ], | ||||||
|  |     "path": "scissors.svg", | ||||||
|  |     "license": "CC-BY 3.0", | ||||||
|  |     "sources": [ | ||||||
|  |       "https://commons.wikimedia.org/wiki/File:Media-floppy.svg" | ||||||
|  |     ] | ||||||
|   } |   } | ||||||
| ] | ] | ||||||
							
								
								
									
										62
									
								
								assets/svg/scissors.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								assets/svg/scissors.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,62 @@ | ||||||
|  | <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||||
|  | <svg | ||||||
|  |    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||||
|  |    xmlns:cc="http://creativecommons.org/ns#" | ||||||
|  |    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||||
|  |    xmlns:svg="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns="http://www.w3.org/2000/svg" | ||||||
|  |    xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" | ||||||
|  |    xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" | ||||||
|  |    version="1.1" | ||||||
|  |    x="0px" | ||||||
|  |    y="0px" | ||||||
|  |    viewBox="0 0 64 62" | ||||||
|  |    xml:space="preserve" | ||||||
|  |    id="svg18" | ||||||
|  |    sodipodi:docname="scissors.svg" | ||||||
|  |    inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)" | ||||||
|  |    width="64" | ||||||
|  |    height="62"><metadata | ||||||
|  |      id="metadata24"><rdf:RDF><cc:Work | ||||||
|  |          rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type | ||||||
|  |            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs | ||||||
|  |      id="defs22" /><sodipodi:namedview | ||||||
|  |      pagecolor="#ffffff" | ||||||
|  |      bordercolor="#666666" | ||||||
|  |      borderopacity="1" | ||||||
|  |      objecttolerance="10" | ||||||
|  |      gridtolerance="10" | ||||||
|  |      guidetolerance="10" | ||||||
|  |      inkscape:pageopacity="0" | ||||||
|  |      inkscape:pageshadow="2" | ||||||
|  |      inkscape:window-width="1920" | ||||||
|  |      inkscape:window-height="1053" | ||||||
|  |      id="namedview20" | ||||||
|  |      showgrid="false" | ||||||
|  |      units="in" | ||||||
|  |      height="0.8" | ||||||
|  |      inkscape:zoom="2.95" | ||||||
|  |      inkscape:cx="-35.288136" | ||||||
|  |      inkscape:cy="40" | ||||||
|  |      inkscape:window-x="0" | ||||||
|  |      inkscape:window-y="27" | ||||||
|  |      inkscape:window-maximized="1" | ||||||
|  |      inkscape:current-layer="svg18" /><path | ||||||
|  |      d="m 31.850939,35.56215 c -1.123789,0 -2.038585,-0.918488 -2.038585,-2.045617 0,-1.129524 0.914796,-2.048011 2.038585,-2.048011 1.123791,0 2.037396,0.918487 2.037396,2.048011 0,1.127129 -0.913605,2.045617 -2.037396,2.045617 z m 0,-2.86578 c -0.450231,0 -0.815672,0.366916 -0.815672,0.820163 0,0.450852 0.365441,0.817768 0.815672,0.817768 0.44904,0 0.81448,-0.366916 0.81448,-0.817768 0,-0.453247 -0.36544,-0.820163 -0.81448,-0.820163 z" | ||||||
|  |      id="path2" | ||||||
|  |      inkscape:connector-curvature="0" /><path | ||||||
|  |      d="m 16.746047,56.399613 c -2.171149,0 -4.172715,-0.872925 -5.862581,-2.604385 -1.7651031,-2.472485 -3.0035427,-6.391049 0.585183,-11.654973 2.216531,-3.244686 8.030148,-6.249561 11.901911,-5.46297 L 44.668845,5.1920716 c 0.259151,-0.384902 0.763126,-0.515601 1.177528,-0.300967 2.548535,1.312983 3.566036,4.3634207 2.315655,6.9450214 -2.161594,4.455748 -5.032574,10.411536 -7.553639,15.641885 -2.484043,5.153608 -4.628921,9.602163 -5.419513,11.216112 -0.298565,0.611526 -0.929131,0.628315 -1.345925,0.640304 -0.73088,0.02158 -1.640899,0.04557 -2.412387,1.654721 l -3.560063,7.266372 c -1.507147,3.069622 -3.686655,5.429394 -6.304455,6.822716 -1.658815,0.882515 -3.281803,1.321377 -4.819999,1.321377 z m 5.288147,-18.010052 c -3.069228,0 -7.427051,2.410134 -9.051234,4.791489 -2.402834,3.522869 -2.636907,6.698013 -0.697443,9.434292 2.242805,2.280635 5.106621,2.59719 8.421862,0.836952 2.273855,-1.213459 4.18227,-3.290253 5.517443,-6.012142 l 3.556482,-7.256783 c 1.221718,-2.549225 3.094305,-2.661938 3.950582,-2.688316 0.907631,-1.863358 2.919945,-6.036125 5.226044,-10.820419 2.521069,-5.230349 5.393242,-11.187336 7.553642,-15.644284 0.690277,-1.4232984 0.314091,-3.0684235 -0.833587,-4.0516624 L 24.520625,38.255265 c -0.235268,0.342934 -0.668782,0.484425 -1.060496,0.352528 -0.426347,-0.151086 -0.907631,-0.218232 -1.425935,-0.218232 z" | ||||||
|  |      id="path4" | ||||||
|  |      inkscape:connector-curvature="0" /><path | ||||||
|  |      d="m 16.740077,52.672898 c -1.177533,0 -2.234446,-0.390896 -2.989212,-1.20147 -1.846314,-1.980865 -1.089158,-5.604458 1.724498,-8.247211 2.816046,-2.645153 6.468067,-3.160751 8.310797,-1.179886 1.846314,1.980865 1.089157,5.604458 -1.723305,8.247211 0,0 0,0 -0.0012,0.0024 -1.662399,1.561188 -3.618582,2.378956 -5.321584,2.378956 z m 4.067619,-9.988262 c -1.234856,0 -2.787382,0.671478 -4.078367,1.882541 -1.994401,1.875347 -2.744392,4.460545 -1.638515,5.647625 1.108266,1.187081 3.724872,0.601933 5.718078,-1.26622 v 0 c 1.993206,-1.875347 2.743195,-4.460544 1.637318,-5.647625 -0.389327,-0.417278 -0.967343,-0.616321 -1.638514,-0.616321 z m 0.628178,6.935424 h 0.02269 z" | ||||||
|  |      id="path6" | ||||||
|  |      inkscape:connector-curvature="0" /><path | ||||||
|  |      d="m 46.54143,57.788136 c -1.620602,0 -3.312855,-0.501213 -5.026604,-1.496441 -2.56406,-1.489246 -4.656387,-3.928158 -6.048885,-7.052937 l -3.286579,-7.38388 c -0.206608,-0.465241 0.0012,-1.009621 0.463367,-1.218258 0.459786,-0.201443 1.004368,0.0024 1.210972,0.46524 l 3.286579,7.383881 c 1.234856,2.769856 3.06445,4.918591 5.292923,6.213589 3.245976,1.882541 6.121735,1.671505 8.542481,-0.630715 1.941856,-2.556419 1.824818,-5.738754 -0.445454,-9.345562 -1.793766,-2.85139 -7.324344,-5.889835 -10.301613,-4.961753 -0.396492,0.119903 -0.826423,-0.03837 -1.047359,-0.3909 l -1.437882,-2.304615 c -0.268703,-0.431664 -0.13853,-1.000027 0.290203,-1.268616 0.431126,-0.270992 0.996006,-0.139091 1.264714,0.290173 l 1.091545,1.750644 c 3.890873,-0.673877 9.597008,2.573208 11.690529,5.901829 3.392872,5.391026 2.01112,9.261627 0.253181,11.559048 -1.740024,1.661915 -3.706955,2.489273 -5.792118,2.489273 z" | ||||||
|  |      id="path8" | ||||||
|  |      inkscape:connector-curvature="0" /><path | ||||||
|  |      d="m 27.243519,33.454182 c -0.352304,0 -0.689083,-0.206238 -0.839559,-0.551571 -1.329203,-3.038448 -3.294941,-7.067327 -5.194994,-10.965507 -1.81168,-3.714724 -3.523043,-7.224406 -4.677883,-9.846774 -1.154843,-2.6247684 -0.02627,-5.6356364 2.570028,-6.8538924 0.4156,-0.196648 0.917185,-0.04796 1.16559,0.345332 L 32.383578,25.003128 c 0.268707,0.431665 0.138531,1.000025 -0.290203,1.268618 -0.43232,0.27099 -0.996006,0.139091 -1.264714,-0.290177 L 19.191874,7.3288165 c -1.183502,0.9400721 -1.619404,2.5696101 -0.982868,4.0180885 1.139315,2.588795 2.843511,6.08169 4.646831,9.780825 1.909609,3.913769 3.882513,7.959434 5.22724,11.036253 0.204218,0.46524 -0.0072,1.007217 -0.470535,1.213459 -0.120619,0.05276 -0.246016,0.07674 -0.369023,0.07674 z" | ||||||
|  |      id="path10" | ||||||
|  |      inkscape:connector-curvature="0" /><path | ||||||
|  |      d="m 46.636969,54.068615 c -0.385742,0 -0.789398,-0.04556 -1.204998,-0.134296 -1.488037,-0.316551 -2.992794,-1.184681 -4.237205,-2.446102 -1.245606,-1.259026 -2.098301,-2.774655 -2.401642,-4.271095 -0.333195,-1.637928 0.01672,-3.069622 0.982872,-4.033673 0.968536,-0.961658 2.400444,-1.290203 4.023433,-0.956859 1.488038,0.31895 2.993988,1.184681 4.238396,2.443706 2.712147,2.748272 3.335545,6.398243 1.418774,8.307167 -0.720135,0.719441 -1.695839,1.091152 -2.81963,1.091152 z M 42.601592,43.938862 c -0.518303,0 -1.101098,0.122307 -1.533417,0.551575 -0.692665,0.690662 -0.597126,1.772224 -0.477699,2.357372 0.232875,1.148713 0.910018,2.335793 1.906025,3.34301 0,0.0024 0,0.0024 0,0.0024 0.996006,1.007221 2.173538,1.697883 3.316436,1.940097 0.578019,0.119908 1.65762,0.225426 2.351479,-0.462841 1.147678,-1.143917 0.493229,-3.755493 -1.428322,-5.702785 -0.996006,-1.007217 -2.174732,-1.695484 -3.316437,-1.937698 -0.218548,-0.04796 -0.506363,-0.09113 -0.818065,-0.09113 z" | ||||||
|  |      id="path12" | ||||||
|  |      inkscape:connector-curvature="0" /></svg> | ||||||
| After Width: | Height: | Size: 6.8 KiB | 
|  | @ -34,7 +34,7 @@ | ||||||
|   "startZoom": 14, |   "startZoom": 14, | ||||||
|   "startLon": 3.2228, |   "startLon": 3.2228, | ||||||
|   "maintainer": "MapComplete", |   "maintainer": "MapComplete", | ||||||
|   "widenfactor": 0.05, |   "widenfactor": 0.01, | ||||||
|   "roamingRenderings": [ |   "roamingRenderings": [ | ||||||
|     { |     { | ||||||
|       "question": { |       "question": { | ||||||
|  | @ -276,7 +276,8 @@ | ||||||
|       }, |       }, | ||||||
|       "tagRenderings": [ |       "tagRenderings": [ | ||||||
|         "images" |         "images" | ||||||
|       ] |       ], | ||||||
|  |       "allowSplit": true | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|  | @ -27,6 +27,14 @@ | ||||||
|     "intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.", |     "intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.", | ||||||
|     "pickTheme": "Pick a theme below to get started." |     "pickTheme": "Pick a theme below to get started." | ||||||
|   }, |   }, | ||||||
|  |   "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", | ||||||
|  |     "hasBeenSplit": "This way has been split" | ||||||
|  |   }, | ||||||
|   "delete": { |   "delete": { | ||||||
|     "delete": "Delete", |     "delete": "Delete", | ||||||
|     "cancel": "Cancel", |     "cancel": "Cancel", | ||||||
|  |  | ||||||
|  | @ -8,7 +8,7 @@ | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", |     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", | ||||||
|     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", |     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/*.json assets/svg/* assets/generated/* assets/layers/*/*.svg assets/tagRendering/*.json assets/themes/*/*.svg assets/themes/*/*.png vendor/* vendor/*/*", | ||||||
|     "test": "ts-node test/TestAll.ts", |     "test": "ts-node test/TestAll.ts", | ||||||
|     "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", |     "init": "npm ci && npm run generate && npm run generate:editor-layer-index && npm run generate:layouts && npm run clean", | ||||||
|     "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", |     "add-weblate-upstream": "git remote add weblate-layers https://hosted.weblate.org/git/mapcomplete/layer-translations/ ; git remote update weblate-layers", | ||||||
|  |  | ||||||
							
								
								
									
										14
									
								
								test.html
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								test.html
									
										
									
									
									
								
							|  | @ -4,10 +4,16 @@ | ||||||
|     <title>Small tests</title> |     <title>Small tests</title> | ||||||
|     <link href="index.css" rel="stylesheet"/> |     <link href="index.css" rel="stylesheet"/> | ||||||
|     <link rel="stylesheet" href="./vendor/leaflet.css"/> |     <link rel="stylesheet" href="./vendor/leaflet.css"/> | ||||||
|     <link href="css/tabbedComponent.css" rel="stylesheet"/> |     <link rel="stylesheet" href="./index.css"/> | ||||||
|     <link href="css/openinghourstable.css" rel="stylesheet"/> |     <link rel="stylesheet" href="./css/userbadge.css"/> | ||||||
|     <link href="css/tagrendering.css" rel="stylesheet"/> |     <link rel="stylesheet" href="./css/tabbedComponent.css"/> | ||||||
|     <link href="css/ReviewElement.css" rel="stylesheet"/> |     <link rel="stylesheet" href="./css/mobile.css"/> | ||||||
|  |     <link rel="stylesheet" href="./css/openinghourstable.css"/> | ||||||
|  |     <link rel="stylesheet" href="./css/tagrendering.css"/> | ||||||
|  |     <link rel="stylesheet" href="css/ReviewElement.css"/> | ||||||
|  |     <link rel="stylesheet" href="vendor/MarkerCluster.css"/> | ||||||
|  |     <link rel="stylesheet" href="vendor/MarkerCluster.Default.css"/> | ||||||
|  |     <meta property="og:type" content="website"> | ||||||
|     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> |     <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> | ||||||
| 
 | 
 | ||||||
| </head> | </head> | ||||||
|  |  | ||||||
							
								
								
									
										169
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										169
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -1,169 +0,0 @@ | ||||||
| import {OsmObject} from "./Logic/Osm/OsmObject"; |  | ||||||
| import DeleteButton from "./UI/Popup/DeleteWizard"; |  | ||||||
| import Combine from "./UI/Base/Combine"; |  | ||||||
| import State from "./State"; |  | ||||||
| import DeleteWizard from "./UI/Popup/DeleteWizard"; |  | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; |  | ||||||
| import {Tag} from "./Logic/Tags/Tag"; |  | ||||||
| import {QueryParameters} from "./Logic/Web/QueryParameters"; |  | ||||||
| import {Translation} from "./UI/i18n/Translation"; |  | ||||||
| import LocationInput from "./UI/Input/LocationInput"; |  | ||||||
| import Loc from "./Models/Loc"; |  | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; |  | ||||||
| import LengthInput from "./UI/Input/LengthInput"; |  | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; |  | ||||||
| /*import ValidatedTextField from "./UI/Input/ValidatedTextField"; |  | ||||||
| import Combine from "./UI/Base/Combine"; |  | ||||||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; |  | ||||||
| import {UIEventSource} from "./Logic/UIEventSource"; |  | ||||||
| import TagRenderingConfig from "./Customizations/JSON/TagRenderingConfig"; |  | ||||||
| import State from "./State"; |  | ||||||
| import TagRenderingQuestion from "./UI/Popup/TagRenderingQuestion"; |  | ||||||
| import {SlideShow} from "./UI/Image/SlideShow"; |  | ||||||
| import {FixedUiElement} from "./UI/Base/FixedUiElement"; |  | ||||||
| import Img from "./UI/Base/Img"; |  | ||||||
| import {AttributedImage} from "./UI/Image/AttributedImage"; |  | ||||||
| import {Imgur} from "./Logic/ImageProviders/Imgur"; |  | ||||||
| import Minimap from "./UI/Base/Minimap"; |  | ||||||
| import Loc from "./Models/Loc"; |  | ||||||
| import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; |  | ||||||
| import ShowDataLayer from "./UI/ShowDataLayer"; |  | ||||||
| import LayoutConfig from "./Customizations/JSON/LayoutConfig"; |  | ||||||
| import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| function TestSlideshow() { |  | ||||||
|     const elems = new UIEventSource([ |  | ||||||
|         new FixedUiElement("A"), |  | ||||||
|         new FixedUiElement("qmsldkfjqmlsdkjfmqlskdjfmqlksdf").SetClass("text-xl"), |  | ||||||
|         new Img("https://i.imgur.com/8lIQ5Hv.jpg"), |  | ||||||
|         new AttributedImage("https://i.imgur.com/y5XudzW.jpg", Imgur.singleton), |  | ||||||
|         new Img("https://www.grunge.com/img/gallery/the-real-reason-your-cat-sleeps-so-much/intro-1601496900.webp") |  | ||||||
|     ]) |  | ||||||
|     new SlideShow(elems).AttachTo("maindiv") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function TestTagRendering() { |  | ||||||
|     State.state = new State(undefined) |  | ||||||
|     const tagsSource = new UIEventSource({ |  | ||||||
|         id: "node/1" |  | ||||||
|     }) |  | ||||||
|     new TagRenderingQuestion( |  | ||||||
|         tagsSource, |  | ||||||
|         new TagRenderingConfig({ |  | ||||||
|             multiAnswer: false, |  | ||||||
|             freeform: { |  | ||||||
|                 key: "valve" |  | ||||||
|             }, |  | ||||||
|             question: "What valves are supported?", |  | ||||||
|             render: "This pump supports {valve}", |  | ||||||
|             mappings: [ |  | ||||||
|                 { |  | ||||||
|                     if: "valve=dunlop", |  | ||||||
|                     then: "This pump supports dunlop" |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                     if: "valve=shrader", |  | ||||||
|                     then: "shrader is supported", |  | ||||||
|                 } |  | ||||||
|             ], |  | ||||||
|              |  | ||||||
|         }, undefined, "test"), |  | ||||||
|         [] |  | ||||||
|     ).AttachTo("maindiv") |  | ||||||
|     new VariableUiElement(tagsSource.map(tags => tags["valves"])).SetClass("alert").AttachTo("extradiv") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function TestAllInputMethods() { |  | ||||||
| 
 |  | ||||||
|     new Combine(ValidatedTextField.tpList.map(tp => { |  | ||||||
|         const tf = ValidatedTextField.InputForType(tp.name); |  | ||||||
| 
 |  | ||||||
|         return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); |  | ||||||
|     })).AttachTo("maindiv") |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| function TestMiniMap() { |  | ||||||
| 
 |  | ||||||
|     const location = new UIEventSource<Loc>({ |  | ||||||
|         lon: 4.84771728515625, |  | ||||||
|         lat: 51.17920846421931, |  | ||||||
|         zoom: 14 |  | ||||||
|     }) |  | ||||||
|     const map0 = new Minimap({ |  | ||||||
|         location: location, |  | ||||||
|         allowMoving: true, |  | ||||||
|         background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[2]) |  | ||||||
|     }) |  | ||||||
|     map0.SetStyle("width: 500px; height: 250px; overflow: hidden; border: 2px solid red") |  | ||||||
|         .AttachTo("maindiv") |  | ||||||
| 
 |  | ||||||
|     const layout = AllKnownLayouts.layoutsList[1] |  | ||||||
|     State.state = new State(layout) |  | ||||||
|     console.log("LAYOUT is", layout.id) |  | ||||||
| 
 |  | ||||||
|     const feature = { |  | ||||||
|             "type": "Feature", |  | ||||||
|             _matching_layer_id: "bike_repair_station", |  | ||||||
|             "properties": { |  | ||||||
|                 id: "node/-1", |  | ||||||
|                 "amenity": "bicycle_repair_station" |  | ||||||
|             }, |  | ||||||
|             "geometry": { |  | ||||||
|                 "type": "Point", |  | ||||||
|                 "coordinates": [ |  | ||||||
|                     4.84771728515625, |  | ||||||
|                     51.17920846421931 |  | ||||||
|                 ] |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
| 
 |  | ||||||
|     ; |  | ||||||
| 
 |  | ||||||
|     State.state.allElements.addOrGetElement(feature) |  | ||||||
| 
 |  | ||||||
|     const featureSource = new UIEventSource([{ |  | ||||||
|         freshness: new Date(), |  | ||||||
|         feature: feature |  | ||||||
|     }]) |  | ||||||
| 
 |  | ||||||
|     new ShowDataLayer( |  | ||||||
|         featureSource, |  | ||||||
|         map0.leafletMap, |  | ||||||
|         new UIEventSource<LayoutConfig>(layout) |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     const map1 = new Minimap({ |  | ||||||
|             location: location, |  | ||||||
|             allowMoving: true, |  | ||||||
|             background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[5]) |  | ||||||
|         }, |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     map1.SetStyle("width: 500px; height: 250px; overflow: hidden; border : 2px solid black") |  | ||||||
|         .AttachTo("extradiv") |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
|     new ShowDataLayer( |  | ||||||
|         featureSource, |  | ||||||
|         map1.leafletMap, |  | ||||||
|         new UIEventSource<LayoutConfig>(layout) |  | ||||||
|     ) |  | ||||||
| 
 |  | ||||||
|     featureSource.ping() |  | ||||||
| } |  | ||||||
| //*/
 |  | ||||||
| 
 |  | ||||||
| const loc = new UIEventSource<Loc>({ |  | ||||||
|     zoom: 24, |  | ||||||
|     lat: 51.21043, |  | ||||||
|     lon: 3.21389 |  | ||||||
| }) |  | ||||||
| const li = new LengthInput( |  | ||||||
|     AvailableBaseLayers.SelectBestLayerAccordingTo(loc, new UIEventSource<string | string[]>("map","photo")), |  | ||||||
|     loc |  | ||||||
| ) |  | ||||||
|     li.SetStyle("height: 30rem; background: aliceblue;") |  | ||||||
|         .AttachTo("maindiv") |  | ||||||
| 
 |  | ||||||
| new VariableUiElement(li.GetValue().map(v => JSON.stringify(v, null, "  "))).AttachTo("extradiv") |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue