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; | ||||
|     public readonly units: Unit[]; | ||||
|     public readonly deletion: DeleteConfig | null; | ||||
|     public readonly allowSplit: boolean | ||||
| 
 | ||||
|     presets: { | ||||
|         title: Translation, | ||||
|  | @ -70,6 +71,7 @@ export default class LayerConfig { | |||
|         context = context + "." + json.id; | ||||
|         const self = this; | ||||
|         this.id = json.id; | ||||
|         this.allowSplit = json.allowSplit ?? false; | ||||
|         this.name = Translations.T(json.name, context + ".name"); | ||||
| 
 | ||||
|         if (json.description !== undefined) { | ||||
|  | @ -373,8 +375,7 @@ export default class LayerConfig { | |||
| 
 | ||||
|     public GenerateLeafletStyle( | ||||
|         tags: UIEventSource<any>, | ||||
|         clickable: boolean, | ||||
|         widthHeight = "100%" | ||||
|         clickable: boolean | ||||
|     ): { | ||||
|         icon: { | ||||
|             html: BaseUIElement; | ||||
|  |  | |||
|  | @ -298,4 +298,9 @@ export interface LayerConfigJson { | |||
|      */ | ||||
|     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; | ||||
|         } | ||||
|         if (this.language.length == 0) { | ||||
|             throw "No languages defined. Define at least one language" | ||||
|             throw `No languages defined. Define at least one language. (${context}.languages)` | ||||
|         } | ||||
|         if (json.title === undefined) { | ||||
|             throw "Title not defined in " + this.id; | ||||
|  |  | |||
|  | @ -513,10 +513,10 @@ export class InitUiElements { | |||
| 
 | ||||
|         const source = new FeaturePipeline( | ||||
|             state.filteredLayers, | ||||
|             State.state.changes, | ||||
|             updater, | ||||
|             state.osmApiFeatureSource, | ||||
|             state.layoutToUse, | ||||
|             state.changes, | ||||
|             state.locationControl, | ||||
|             state.selectedElement | ||||
|         ); | ||||
|  |  | |||
							
								
								
									
										36
									
								
								Logic/Actors/ChangeToElementsActor.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								Logic/Actors/ChangeToElementsActor.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,36 @@ | |||
| import {ElementStorage} from "../ElementStorage"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| 
 | ||||
| export default class ChangeToElementsActor { | ||||
|     constructor(changes: Changes, allElements: ElementStorage) { | ||||
|         changes.pendingChanges.addCallbackAndRun(changes => { | ||||
|             for (const change of changes) { | ||||
|                 const id = change.type + "/" + change.id; | ||||
|                 if (!allElements.has(id)) { | ||||
|                     continue; // 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>) { | ||||
|         const self = this; | ||||
|         this.lastChange = new Date(); | ||||
|         changes.pending.addCallback(() => { | ||||
|         changes.pendingChanges.addCallback(() => { | ||||
|             self.lastChange = new Date(); | ||||
| 
 | ||||
|             window.setTimeout(() => { | ||||
|  | @ -54,7 +54,7 @@ export default class PendingChangesUploader { | |||
| 
 | ||||
| 
 | ||||
|         function onunload(e) { | ||||
|             if (changes.pending.data.length == 0) { | ||||
|             if(changes.pendingChanges.data.length == 0){ | ||||
|                 return; | ||||
|             } | ||||
|             changes.flushChanges("onbeforeunload - probably closing or something similar"); | ||||
|  |  | |||
|  | @ -13,7 +13,7 @@ export default class SelectedFeatureHandler { | |||
|     private readonly _hash: UIEventSource<string>; | ||||
|     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; | ||||
|      | ||||
|     constructor(hash: UIEventSource<string>,  | ||||
|  | @ -60,7 +60,9 @@ export default class SelectedFeatureHandler { | |||
|         if(hash === undefined || SelectedFeatureHandler._no_trigger_on.indexOf(hash) >= 0){ | ||||
|             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 => { | ||||
|             const centerpoint = element.centerpoint(); | ||||
|             console.log("Zooming to location for select point: ", centerpoint) | ||||
|  | @ -68,6 +70,9 @@ export default class SelectedFeatureHandler { | |||
|             location.data.lon = centerpoint[1] | ||||
|             location.ping(); | ||||
|         }) | ||||
|         }catch(e){ | ||||
|             console.error("Could not download OSM-object with id", hash, " - probably a weird hash") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     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 {UIEventSource} from "../UIEventSource"; | ||||
| import LocalStorageSaver from "./LocalStorageSaver"; | ||||
| import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||
| import LocalStorageSource from "./LocalStorageSource"; | ||||
| import LayoutConfig from "../../Customizations/JSON/LayoutConfig"; | ||||
| import Loc from "../../Models/Loc"; | ||||
|  | @ -14,6 +13,8 @@ import GeoJsonSource from "./GeoJsonSource"; | |||
| import MetaTaggingFeatureSource from "./MetaTaggingFeatureSource"; | ||||
| import RegisteringFeatureSource from "./RegisteringFeatureSource"; | ||||
| import FilteredLayer from "../../Models/FilteredLayer"; | ||||
| import {Changes} from "../Osm/Changes"; | ||||
| import ChangeApplicator from "./ChangeApplicator"; | ||||
| 
 | ||||
| export default class FeaturePipeline implements FeatureSource { | ||||
| 
 | ||||
|  | @ -22,10 +23,10 @@ export default class FeaturePipeline implements FeatureSource { | |||
|     public readonly name = "FeaturePipeline" | ||||
| 
 | ||||
|     constructor(flayers: UIEventSource<FilteredLayer[]>, | ||||
|                 changes: Changes, | ||||
|                 updater: FeatureSource, | ||||
|                 fromOsmApi: FeatureSource, | ||||
|                 layout: UIEventSource<LayoutConfig>, | ||||
|                 newPoints: FeatureSource, | ||||
|                 locationControl: UIEventSource<Loc>, | ||||
|                 selectedElement: UIEventSource<any>) { | ||||
| 
 | ||||
|  | @ -41,7 +42,9 @@ export default class FeaturePipeline implements FeatureSource { | |||
|                     new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|                         new FeatureDuplicatorPerLayer(flayers, | ||||
|                             new RegisteringFeatureSource( | ||||
|                                 updater) | ||||
|                                 new ChangeApplicator( | ||||
|                                     updater, changes | ||||
|                                 )) | ||||
|                         )), layout)); | ||||
| 
 | ||||
|         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||
|  | @ -49,8 +52,7 @@ export default class FeaturePipeline implements FeatureSource { | |||
|             .map(geojsonSource => { | ||||
|                 let source = new RegisteringFeatureSource( | ||||
|                     new FeatureDuplicatorPerLayer(flayers, | ||||
|                             geojsonSource | ||||
|                     )); | ||||
|                         new ChangeApplicator(geojsonSource, changes))); | ||||
|                 if (!geojsonSource.isOsmCache) { | ||||
|                     source = new MetaTaggingFeatureSource(allLoadedFeatures, source, updater.features); | ||||
|                 } | ||||
|  | @ -58,25 +60,24 @@ export default class FeaturePipeline implements FeatureSource { | |||
|             }); | ||||
| 
 | ||||
|         const amendedLocalStorageSource = | ||||
|             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)) | ||||
|             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new ChangeApplicator(new LocalStorageSource(layout), changes)) | ||||
|             )); | ||||
| 
 | ||||
|         newPoints = new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|             new FeatureDuplicatorPerLayer(flayers, | ||||
|                 new RegisteringFeatureSource(newPoints))); | ||||
| 
 | ||||
|         const amendedOsmApiSource = new RememberingSource( | ||||
|             new MetaTaggingFeatureSource(allLoadedFeatures, | ||||
|                 new FeatureDuplicatorPerLayer(flayers, | ||||
| 
 | ||||
|                     new RegisteringFeatureSource(fromOsmApi)))); | ||||
|                     new RegisteringFeatureSource(new ChangeApplicator(fromOsmApi, changes, | ||||
|                         { | ||||
|                             // We lump in the new points here
 | ||||
|                             generateNewGeometries: true | ||||
|                         } | ||||
|                     ))))); | ||||
| 
 | ||||
|         const merged = | ||||
|             new FeatureSourceMerger([ | ||||
|                 amendedOverpassSource, | ||||
|                 amendedOsmApiSource, | ||||
|                 amendedLocalStorageSource, | ||||
|                 newPoints, | ||||
|                 ...geojsonSources | ||||
|             ]); | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| import FeatureSource from "./FeatureSource"; | ||||
| 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 { | ||||
| 
 | ||||
|     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 true; | ||||
|             }); | ||||
|             console.log( | ||||
|                 "Filtering layer source: input: ", | ||||
|                 upstream.features.data?.length, | ||||
|                 "output:", | ||||
|                 newFeatures.length | ||||
|             ); | ||||
| 
 | ||||
|             self.features.setData(newFeatures); | ||||
|             if (missingLayers.size > 0) { | ||||
|                 console.error( | ||||
|  |  | |||
|  | @ -15,15 +15,19 @@ export default class OsmApiFeatureSource implements FeatureSource { | |||
| 
 | ||||
| 
 | ||||
|     public load(id: string) { | ||||
|         if(id.indexOf("-") >= 0){ | ||||
|         if (id.indexOf("-") >= 0) { | ||||
|             // Newly added point - not yet in OSM
 | ||||
|             return; | ||||
|         } | ||||
|         console.debug("Downloading", id, "from the OSM-API") | ||||
|         OsmObject.DownloadObject(id).addCallbackAndRunD(element => { | ||||
|             const geojson = element.asGeoJson(); | ||||
|             geojson.id = geojson.properties.id; | ||||
|             this.features.setData([{feature: geojson, freshness: element.timestamp}]) | ||||
|             try { | ||||
|                 const geojson = element.asGeoJson(); | ||||
|                 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); | ||||
|             console.log("Loading OSM data tile", z, x, y, " with bounds", bounds) | ||||
|             OsmObject.LoadArea(bounds, objects => { | ||||
|                 const keptGeoJson: {feature:any, freshness: Date}[] = [] | ||||
|                 const keptGeoJson: { feature: any, freshness: Date }[] = [] | ||||
|                 // Which layer does the object match?
 | ||||
|                 for (const object of objects) { | ||||
| 
 | ||||
|  | @ -69,7 +73,7 @@ export default class OsmApiFeatureSource implements FeatureSource { | |||
|                         if (doesMatch) { | ||||
|                             const geoJson = object.asGeoJson(); | ||||
|                             geoJson._matching_layer_id = layer.id | ||||
|                             keptGeoJson.push({feature: geoJson, freshness:  object.timestamp}) | ||||
|                             keptGeoJson.push({feature: geoJson, freshness: object.timestamp}) | ||||
|                             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 {Translation} from "../../UI/i18n/Translation"; | ||||
| import Translations from "../../UI/i18n/Translations"; | ||||
| import {OsmObject} from "./OsmObject"; | ||||
| import State from "../../State"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import {UIEventSource} from "../../UIEventSource"; | ||||
| import {Translation} from "../../../UI/i18n/Translation"; | ||||
| import State from "../../../State"; | ||||
| import {OsmObject} from "../OsmObject"; | ||||
| import Translations from "../../../UI/i18n/Translations"; | ||||
| import Constants from "../../../Models/Constants"; | ||||
| 
 | ||||
| export default class DeleteAction { | ||||
| 
 | ||||
|  | @ -30,7 +30,7 @@ export default class DeleteAction { | |||
|      * Does actually delete the feature; returns the event source 'this.isDeleted' | ||||
|      * If deletion is not allowed, triggers the callback instead | ||||
|      */ | ||||
|     public DoDelete(reason: string, onNotAllowed : () => void): UIEventSource<boolean> { | ||||
|     public DoDelete(reason: string, onNotAllowed : () => void): void { | ||||
|         const isDeleted = this.isDeleted | ||||
|         const self = this; | ||||
|         let deletionStarted = false; | ||||
|  | @ -75,8 +75,6 @@ export default class DeleteAction { | |||
| 
 | ||||
|             } | ||||
|         ) | ||||
| 
 | ||||
|         return isDeleted; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
							
								
								
									
										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 {Utils} from "../../Utils"; | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import FeatureSource from "../FeatureSource/FeatureSource"; | ||||
| import {TagsFilter} from "../Tags/TagsFilter"; | ||||
| import {Tag} from "../Tags/Tag"; | ||||
| import {OsmConnection} from "./OsmConnection"; | ||||
| import OsmChangeAction from "./Actions/OsmChangeAction"; | ||||
| import {ChangeDescription} from "./Actions/ChangeDescription"; | ||||
| import {Utils} from "../../Utils"; | ||||
| import {LocalStorageSource} from "../Web/LocalStorageSource"; | ||||
| 
 | ||||
| /** | ||||
|  * Handles all changes made to OSM. | ||||
|  * Needs an authenticator via OsmConnection | ||||
|  */ | ||||
| export class Changes implements FeatureSource { | ||||
| export class Changes { | ||||
| 
 | ||||
| 
 | ||||
|     private static _nextId = -1; // Newly assigned ID's are negative
 | ||||
|     public readonly name = "Newly added features" | ||||
|     /** | ||||
|      * The newly created points, as a FeatureSource | ||||
|      * All the newly created features as featureSource + all the modified features | ||||
|      */ | ||||
|     public features = new UIEventSource<{ feature: any, freshness: Date }[]>([]); | ||||
|     /** | ||||
|      * All the pending changes | ||||
|      */ | ||||
|     public readonly pending = LocalStorageSource.GetParsed<{ elementId: string, key: string, value: string }[]>("pending-changes", []) | ||||
| 
 | ||||
|     /** | ||||
|      * All the pending new objects to upload | ||||
|      */ | ||||
|     private readonly newObjects = LocalStorageSource.GetParsed<{ id: number, lat: number, lon: number }[]>("newObjects", []) | ||||
| 
 | ||||
|     public readonly pendingChanges = LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||
|     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(" ")) { | ||||
|             console.warn("Tag starts with or ends with a space - trimming anyway") | ||||
|         } | ||||
| 
 | ||||
|         return {k: key.trim(), v: value.trim()}; | ||||
|     constructor() { | ||||
|         | ||||
|     } | ||||
| 
 | ||||
|     private static createChangesetFor(csId: string, | ||||
|                                      allChanges: { | ||||
|                                          modifiedObjects: OsmObject[], | ||||
|                                          newObjects: OsmObject[], | ||||
|                                          deletedObjects: OsmObject[] | ||||
|                                      }): string { | ||||
| 
 | ||||
|     addTag(elementId: string, tagsFilter: TagsFilter, | ||||
|            tags?: UIEventSource<any>) { | ||||
|         const eventSource = tags ?? State.state?.allElements.getEventSourceById(elementId); | ||||
|         const elementTags = eventSource.data; | ||||
|         const changes = tagsFilter.asChange(elementTags).map(Changes.checkChange) | ||||
|         if (changes.length == 0) { | ||||
|             return; | ||||
|         const changedElements = allChanges.modifiedObjects ?? [] | ||||
|         const newElements = allChanges.newObjects ?? [] | ||||
|         const deletedElements = allChanges.deletedObjects ?? [] | ||||
| 
 | ||||
|         let changes = `<osmChange version='0.6' generator='Mapcomplete ${Constants.vNumber}'>`; | ||||
|         if (newElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<create>\n" + | ||||
|                 newElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "</create>"; | ||||
|         } | ||||
|         if (changedElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<modify>\n" + | ||||
|                 changedElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</modify>"; | ||||
|         } | ||||
| 
 | ||||
|         if (deletedElements.length > 0) { | ||||
|             changes += | ||||
|                 "\n<deleted>\n" + | ||||
|                 deletedElements.map(e => e.ChangesetXML(csId)).join("\n") + | ||||
|                 "\n</deleted>" | ||||
|         } | ||||
| 
 | ||||
|         changes += "</osmChange>"; | ||||
|         return changes; | ||||
|     } | ||||
| 
 | ||||
|     private static GetNeededIds(changes: ChangeDescription[]) { | ||||
|         return Utils.Dedup(changes.filter(c => c.id >= 0) | ||||
|             .map(c => c.type + "/" + c.id)) | ||||
|     } | ||||
| 
 | ||||
|     private 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) { | ||||
|             if (elementTags[change.k] !== change.v) { | ||||
|                 elementTags[change.k] = change.v; | ||||
|                 console.log("Applied ", change.k, "=", change.v) | ||||
|                 // We use 'elementTags.id' here, as we might have retrieved with the id 'node/-1' as new point, but should use the rewritten id
 | ||||
|                 this.pending.data.push({elementId: elementTags.id, key: change.k, value: change.v}); | ||||
|             const id = change.type + "/" + change.id | ||||
|             if (!objects.has(id)) { | ||||
|                 if(change.id >= 0){ | ||||
|                     throw "Did not get an object that should be known: "+id | ||||
|                 } | ||||
|                 // 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 | ||||
|      */ | ||||
|     public flushChanges(flushreason: string = undefined) { | ||||
|         if (this.pending.data.length === 0) { | ||||
|         if (this.pendingChanges.data.length === 0) { | ||||
|             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) { | ||||
|             console.log("Is already uploading... Abort") | ||||
|             return; | ||||
|         } | ||||
|          | ||||
|        | ||||
|         this.isUploading.setData(true) | ||||
| 
 | ||||
|         console.log("Beginning upload..."); | ||||
|         | ||||
|         console.log("Beginning upload... "+flushreason ?? ""); | ||||
|         // 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 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; | ||||
|         let neededIds: string[] = []; | ||||
|         for (const change of pending) { | ||||
|             const id = change.elementId; | ||||
|             if (parseFloat(id.split("/")[1]) < 0) { | ||||
|                 // New element - we don't have to download this
 | ||||
|             } else { | ||||
|                 neededIds.push(id); | ||||
|             }  = self.CreateChangesetObjects(pending, osmObjects) | ||||
|             if (changes.newObjects.length + changes.deletedObjects.length + changes.modifiedObjects.length === 0) { | ||||
|                 console.log("No changes to be made") | ||||
|                 self.pendingChanges.setData([]) | ||||
|                 self.isUploading.setData(false) | ||||
|                 return true; // Unregister the callback
 | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         neededIds = Utils.Dedup(neededIds); | ||||
|         OsmObject.DownloadAll(neededIds).addCallbackAndRunD(knownElements => { | ||||
|             self.uploadChangesWithLatestVersions(knownElements) | ||||
|         }) | ||||
| 
 | ||||
|             State.state.osmConnection.UploadChangeset( | ||||
|                 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(); | ||||
| 
 | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -23,7 +23,7 @@ export abstract class OsmObject { | |||
|         this.id = id; | ||||
|         this.type = type; | ||||
|         this.tags = { | ||||
|             id: id | ||||
|             id: `${this.type}/${id}` | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | @ -51,7 +51,10 @@ export abstract class OsmObject { | |||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = splitted[1]; | ||||
|         const idN = Number(splitted[1]); | ||||
|         if(idN <0){ | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         OsmObject.objectCache.set(id, src); | ||||
|         const newContinuation = (element: OsmObject) => { | ||||
|  | @ -68,6 +71,8 @@ export abstract class OsmObject { | |||
|             case("relation"): | ||||
|                 new OsmRelation(idN).Download(newContinuation); | ||||
|                 break; | ||||
|             default: | ||||
|                 throw "Invalid object type:" + type + id; | ||||
| 
 | ||||
|         } | ||||
|         return src; | ||||
|  | @ -103,7 +108,7 @@ export abstract class OsmObject { | |||
|         if (OsmObject.referencingRelationsCache.has(id)) { | ||||
|             return OsmObject.referencingRelationsCache.get(id); | ||||
|         } | ||||
|         const relsSrc = new UIEventSource<OsmRelation[]>([]) | ||||
|         const relsSrc = new UIEventSource<OsmRelation[]>(undefined) | ||||
|         OsmObject.referencingRelationsCache.set(id, relsSrc); | ||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${id}/relations`) | ||||
|             .then(data => { | ||||
|  | @ -123,7 +128,7 @@ export abstract class OsmObject { | |||
|         } | ||||
|         const splitted = id.split("/"); | ||||
|         const type = splitted[0]; | ||||
|         const idN = splitted[1]; | ||||
|         const idN = Number(splitted[1]); | ||||
|         const src = new UIEventSource<OsmObject[]>([]); | ||||
|         OsmObject.historyCache.set(id, src); | ||||
|         Utils.downloadJson(`${OsmObject.backendURL}api/0.6/${type}/${idN}/history`).then(data => { | ||||
|  | @ -312,20 +317,6 @@ export abstract class OsmObject { | |||
|         return this; | ||||
|     } | ||||
| 
 | ||||
|     public addTag(k: string, v: string): void { | ||||
|         if (k in this.tags) { | ||||
|             const oldV = this.tags[k]; | ||||
|             if (oldV == v) { | ||||
|                 return; | ||||
|             } | ||||
|             console.log("Overwriting ", oldV, " with ", v, " for key ", k) | ||||
|         } | ||||
|         this.tags[k] = v; | ||||
|         if (v === undefined || v === "") { | ||||
|             delete this.tags[k]; | ||||
|         } | ||||
|         this.changed = true; | ||||
|     } | ||||
| 
 | ||||
|     abstract ChangesetXML(changesetId: string): string; | ||||
| 
 | ||||
|  | @ -360,7 +351,7 @@ export class OsmNode extends OsmObject { | |||
|     lat: number; | ||||
|     lon: number; | ||||
| 
 | ||||
|     constructor(id) { | ||||
|     constructor(id: number) { | ||||
|         super("node", id); | ||||
| 
 | ||||
|     } | ||||
|  | @ -368,9 +359,9 @@ export class OsmNode extends OsmObject { | |||
|     ChangesetXML(changesetId: string): string { | ||||
|         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 + | ||||
|             '        </node>\n'; | ||||
|             '    </node>\n'; | ||||
|     } | ||||
| 
 | ||||
|     SaveExtraData(element) { | ||||
|  | @ -413,9 +404,8 @@ export class OsmWay extends OsmObject { | |||
|     lat: number; | ||||
|     lon: number; | ||||
| 
 | ||||
|     constructor(id) { | ||||
|     constructor(id: number) { | ||||
|         super("way", id); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     centerpoint(): [number, number] { | ||||
|  | @ -432,7 +422,7 @@ export class OsmWay extends OsmObject { | |||
|         return '    <way id="' + this.id + '" changeset="' + changesetId + '" ' + this.VersionXML() + '>\n' + | ||||
|             nds + | ||||
|             tags + | ||||
|             '        </way>\n'; | ||||
|             '    </way>\n'; | ||||
|     } | ||||
| 
 | ||||
|     SaveExtraData(element, allNodes: OsmNode[]) { | ||||
|  | @ -458,7 +448,7 @@ export class OsmWay extends OsmObject { | |||
|         this.nodes = element.nodes; | ||||
|     } | ||||
| 
 | ||||
|     asGeoJson() { | ||||
|     public asGeoJson() { | ||||
|         return { | ||||
|             "type": "Feature", | ||||
|             "properties": this.tags, | ||||
|  | @ -480,11 +470,14 @@ export class OsmWay 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); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     centerpoint(): [number, number] { | ||||
|  |  | |||
|  | @ -24,12 +24,19 @@ export class Tag extends TagsFilter { | |||
| 
 | ||||
|     matchesProperties(properties: any): boolean { | ||||
|         for (const propertiesKey in properties) { | ||||
|             if(!properties.hasOwnProperty(propertiesKey)){ | ||||
|                 continue | ||||
|             } | ||||
|             if (this.key === propertiesKey) { | ||||
|                 const value = properties[propertiesKey]; | ||||
|                 if(value === undefined){ | ||||
|                     continue | ||||
|                 } | ||||
|                 return value === this.value; | ||||
|             } | ||||
|         } | ||||
|         // The tag was not found
 | ||||
|          | ||||
|         if (this.value === "") { | ||||
|             // and it shouldn't be found!
 | ||||
|             return true; | ||||
|  |  | |||
|  | @ -2,21 +2,27 @@ import {Utils} from "../Utils"; | |||
| 
 | ||||
| export class UIEventSource<T> { | ||||
| 
 | ||||
|     private static allSources: UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||
|     public data: T; | ||||
|     public trace: boolean; | ||||
|     private readonly tag: string; | ||||
|     private _callbacks = []; | ||||
|      | ||||
|     private static allSources : UIEventSource<any>[] = UIEventSource.PrepPerf(); | ||||
|      | ||||
|     static PrepPerf() : UIEventSource<any>[]{ | ||||
|         if(Utils.runningFromConsole){ | ||||
|     private _callbacks: ((t: T) => (boolean | void | any)) [] = []; | ||||
| 
 | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         this.tag = tag; | ||||
|         this.data = data; | ||||
|         UIEventSource.allSources.push(this); | ||||
|     } | ||||
| 
 | ||||
|     static PrepPerf(): UIEventSource<any>[] { | ||||
|         if (Utils.runningFromConsole) { | ||||
|             return []; | ||||
|         } | ||||
|         // @ts-ignore
 | ||||
|         window.mapcomplete_performance = () => { | ||||
|             console.log(UIEventSource.allSources.length, "uieventsources created"); | ||||
|             const copy = [...UIEventSource.allSources]; | ||||
|             copy.sort((a,b) => b._callbacks.length - a._callbacks.length); | ||||
|             copy.sort((a, b) => b._callbacks.length - a._callbacks.length); | ||||
|             console.log("Topten is:") | ||||
|             for (let i = 0; i < 10; i++) { | ||||
|                 console.log(copy[i].tag, copy[i]); | ||||
|  | @ -26,12 +32,6 @@ export class UIEventSource<T> { | |||
|         return []; | ||||
|     } | ||||
| 
 | ||||
|     constructor(data: T, tag: string = "") { | ||||
|         this.tag = tag; | ||||
|         this.data = data; | ||||
|         UIEventSource.allSources.push(this); | ||||
|     } | ||||
| 
 | ||||
|     public static flatten<X>(source: UIEventSource<UIEventSource<X>>, possibleSources: UIEventSource<any>[]): UIEventSource<X> { | ||||
|         const sink = new UIEventSource<X>(source.data?.data); | ||||
| 
 | ||||
|  | @ -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) { | ||||
|             // This ^^^ actually works!
 | ||||
|             throw "Don't add console.log directly as a callback - you'll won't be able to find it afterwards. Wrap it in a lambda instead." | ||||
|         } | ||||
|         if (this.trace) { | ||||
|             console.trace("Added a callback") | ||||
|         } | ||||
|         this._callbacks.push(callback); | ||||
|         return this; | ||||
|     } | ||||
|  | @ -87,8 +96,21 @@ export class UIEventSource<T> { | |||
|     } | ||||
| 
 | ||||
|     public ping(): void { | ||||
|         let toDelete = undefined | ||||
|         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), | ||||
|                   extraSources: UIEventSource<any>[] = [], | ||||
|                   g: ((j:J, t:T) => T) = undefined): UIEventSource<J> { | ||||
|                   g: ((j: J, t: T) => T) = undefined): UIEventSource<J> { | ||||
|         const self = this; | ||||
| 
 | ||||
|         const newSource = new UIEventSource<J>( | ||||
|             f(this.data), | ||||
|             "map("+this.tag+")" | ||||
|             "map(" + this.tag + ")" | ||||
|         ); | ||||
| 
 | ||||
|         const update = function () { | ||||
|  | @ -159,10 +181,10 @@ export class UIEventSource<T> { | |||
|         return newSource; | ||||
|     } | ||||
| 
 | ||||
|     addCallbackAndRunD(callback: (data :T ) => void) { | ||||
|     addCallbackAndRunD(callback: (data: T) => void) { | ||||
|         this.addCallbackAndRun(data => { | ||||
|             if(data !== undefined && data !== null){ | ||||
|                 callback(data) | ||||
|             if (data !== undefined && data !== null) { | ||||
|               return  callback(data) | ||||
|             } | ||||
|         }) | ||||
|     } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ import { Utils } from "../Utils"; | |||
| 
 | ||||
| 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
 | ||||
|     public static userJourney = { | ||||
|  |  | |||
							
								
								
									
										22
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										22
									
								
								State.ts
									
										
									
									
									
								
							|  | @ -14,14 +14,13 @@ import Loc from "./Models/Loc"; | |||
| import Constants from "./Models/Constants"; | ||||
| 
 | ||||
| import OverpassFeatureSource from "./Logic/Actors/OverpassFeatureSource"; | ||||
| import LayerConfig from "./Customizations/JSON/LayerConfig"; | ||||
| import TitleHandler from "./Logic/Actors/TitleHandler"; | ||||
| import PendingChangesUploader from "./Logic/Actors/PendingChangesUploader"; | ||||
| import {Relation} from "./Logic/Osm/ExtractRelations"; | ||||
| import OsmApiFeatureSource from "./Logic/FeatureSource/OsmApiFeatureSource"; | ||||
| import FeaturePipeline from "./Logic/FeatureSource/FeaturePipeline"; | ||||
| import { TagsFilter } from "./Logic/Tags/TagsFilter"; | ||||
| import FilteredLayer from "./Models/FilteredLayer"; | ||||
| import ChangeToElementsActor from "./Logic/Actors/ChangeToElementsActor"; | ||||
| 
 | ||||
| /** | ||||
|  * Contains the global state: a bunch of UI-event sources | ||||
|  | @ -31,7 +30,7 @@ export default class State { | |||
|     // The singleton of the global 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> | ||||
|  | @ -44,7 +43,7 @@ export default class State { | |||
|     /** | ||||
|      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 | ||||
|      */ | ||||
|  | @ -62,7 +61,7 @@ export default class State { | |||
| 
 | ||||
|     public osmApiFeatureSource: OsmApiFeatureSource; | ||||
| 
 | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([]); | ||||
|     public filteredLayers: UIEventSource<FilteredLayer[]> = new UIEventSource<FilteredLayer[]>([],"filteredLayers"); | ||||
| 
 | ||||
|     /** | ||||
|      The latest element that was selected | ||||
|  | @ -102,7 +101,7 @@ export default class State { | |||
|     /** | ||||
|      * 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 readonly backgroundLayerId: UIEventSource<string>; | ||||
| 
 | ||||
|  | @ -187,11 +186,13 @@ export default class State { | |||
|                 ).syncWith(LocalStorageSource.Get("lon")) | ||||
|             ); | ||||
| 
 | ||||
|             this.locationControl = new UIEventSource<Loc>({ | ||||
|             this.locationControl.setData({ | ||||
|                 zoom: Utils.asFloat(zoom.data), | ||||
|                 lat: Utils.asFloat(lat.data), | ||||
|                 lon: Utils.asFloat(lon.data), | ||||
|             }).addCallback((latlonz) => { | ||||
|             }) | ||||
|             this.locationControl.addCallback((latlonz) => { | ||||
|                 // Sync th location controls
 | ||||
|                 zoom.setData(latlonz.zoom); | ||||
|                 lat.setData(latlonz.lat); | ||||
|                 lon.setData(latlonz.lon); | ||||
|  | @ -371,7 +372,10 @@ export default class State { | |||
| 
 | ||||
|         this.allElements = new ElementStorage(); | ||||
|         this.changes = new Changes(); | ||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource(); | ||||
|          | ||||
|         new ChangeToElementsActor(this.changes, this.allElements) | ||||
|          | ||||
|         this.osmApiFeatureSource = new OsmApiFeatureSource() | ||||
| 
 | ||||
|         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() | ||||
|         options = options ?? {} | ||||
|         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._allowMoving = options.allowMoving ?? true; | ||||
|         this._leafletoptions = options.leafletOptions ?? {} | ||||
|  | @ -43,6 +43,7 @@ export default class Minimap extends BaseUIElement { | |||
|         div.style.width = "100%" | ||||
|         div.style.minWidth = "40px" | ||||
|         div.style.minHeight = "40px" | ||||
|         div.style.position = "relative" | ||||
|         const wrapper = document.createElement("div") | ||||
|         wrapper.appendChild(div) | ||||
|         const self = this; | ||||
|  |  | |||
|  | @ -64,13 +64,11 @@ export default class PersonalLayersPanel extends VariableUiElement { | |||
|     private static CreateLayerToggle(layer: LayerConfig): Toggle { | ||||
|         let icon :BaseUIElement =new Combine([ layer.GenerateLeafletStyle( | ||||
|             new UIEventSource<any>({id: "node/-1"}), | ||||
|             false, | ||||
|             "2em" | ||||
|             false | ||||
|         ).icon.html]).SetClass("relative") | ||||
|         let iconUnset =new Combine([ layer.GenerateLeafletStyle( | ||||
|             new UIEventSource<any>({id: "node/-1"}), | ||||
|             false, | ||||
|             "2em" | ||||
|             false | ||||
|         ).icon.html]).SetClass("relative") | ||||
| 
 | ||||
|         iconUnset.SetStyle("opacity:0.1") | ||||
|  |  | |||
|  | @ -20,6 +20,8 @@ import LocationInput from "../Input/LocationInput"; | |||
| import {InputElement} from "../Input/InputElement"; | ||||
| import Loc from "../../Models/Loc"; | ||||
| import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; | ||||
| import CreateNewNodeAction from "../../Logic/Osm/Actions/CreateNewNodeAction"; | ||||
| import Hash from "../../Logic/Web/Hash"; | ||||
| 
 | ||||
| /* | ||||
| * 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); | ||||
|         isShown.addCallback(_ => selectedPreset.setData(undefined)) // Clear preset selection when the UI is closed/opened
 | ||||
| 
 | ||||
|         function createNewPoint(tags: any[], location: { lat: number, lon: number }) { | ||||
|             let feature = State.state.changes.createElement(tags, location.lat, location.lon); | ||||
|             State.state.selectedElement.setData(feature); | ||||
|         } | ||||
| 
 | ||||
|         const presetsOverview = SimpleAddUI.CreateAllPresetsPanel(selectedPreset) | ||||
| 
 | ||||
|         const addUi = new VariableUiElement( | ||||
|  | @ -75,8 +72,16 @@ export default class SimpleAddUI extends Toggle { | |||
|                     } | ||||
|                     return SimpleAddUI.CreateConfirmButton(preset, | ||||
|                         (tags, location) => { | ||||
|                             createNewPoint(tags, location) | ||||
|                         const newElementAction = new CreateNewNodeAction(tags, location.lat, location.lon) | ||||
|                             State.state.changes.applyAction(newElementAction) | ||||
|                             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) | ||||
|                         }) | ||||
|  | @ -121,16 +126,16 @@ export default class SimpleAddUI extends Toggle { | |||
|                 lon: location.data.lon, | ||||
|                 zoom: 19 | ||||
|             }); | ||||
|              | ||||
| 
 | ||||
|             let backgroundLayer = undefined; | ||||
|             if(preset.preciseInput.preferredBackground){ | ||||
|                backgroundLayer= AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) | ||||
|             if (preset.preciseInput.preferredBackground) { | ||||
|                 backgroundLayer = AvailableBaseLayers.SelectBestLayerAccordingTo(locationSrc, new UIEventSource<string | string[]>(preset.preciseInput.preferredBackground)) | ||||
|             } | ||||
|              | ||||
| 
 | ||||
|             preciseInput = new LocationInput({ | ||||
|                 mapBackground: backgroundLayer, | ||||
|                 centerLocation:locationSrc | ||||
|                      | ||||
|                 centerLocation: locationSrc | ||||
| 
 | ||||
|             }) | ||||
|             preciseInput.SetClass("h-32 rounded-xl overflow-hidden border border-gray").SetStyle("height: 12rem;") | ||||
|         } | ||||
|  | @ -145,7 +150,7 @@ export default class SimpleAddUI extends Toggle { | |||
|             .onClick(() => { | ||||
|                 confirm(preset.tags, (preciseInput?.GetValue() ?? location).data); | ||||
|             }); | ||||
|          | ||||
| 
 | ||||
|         if (preciseInput !== undefined) { | ||||
|             confirmButton = new Combine([preciseInput, confirmButton]) | ||||
|         } | ||||
|  | @ -241,7 +246,7 @@ export default class SimpleAddUI extends Toggle { | |||
|             for (const preset of presets) { | ||||
| 
 | ||||
|                 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"); | ||||
|                 const presetInfo: PresetInfo = { | ||||
|                     tags: preset.tags, | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import Combine from "../Base/Combine"; | |||
| import State from "../../State"; | ||||
| import Svg from "../../Svg"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| 
 | ||||
| export default class DeleteImage extends Toggle { | ||||
|  | @ -15,14 +16,17 @@ export default class DeleteImage extends Toggle { | |||
|             .SetClass("rounded-full p-1") | ||||
|             .SetStyle("color:white;background:#ff8c8c") | ||||
|             .onClick(() => { | ||||
|                 State.state?.changes?.addTag(tags.data.id, new Tag(key, oldValue), tags); | ||||
|                 State.state?.changes?. | ||||
|                     applyAction(new ChangeTagAction(tags.data.id, new Tag(key, oldValue), tags.data)) | ||||
|             }); | ||||
| 
 | ||||
|         const deleteButton = Translations.t.image.doDelete.Clone() | ||||
|             .SetClass("block w-full pl-4 pr-4") | ||||
|             .SetStyle("color:white;background:#ff8c8c; border-top-left-radius:30rem; border-top-right-radius: 30rem;") | ||||
|             .onClick(() => { | ||||
|                 State.state?.changes?.addTag(tags.data.id, new Tag(key, ""), tags); | ||||
|                 State.state?.changes?.applyAction( | ||||
|                 new ChangeTagAction(    tags.data.id, new Tag(key, ""), tags.data) | ||||
|                 ) | ||||
|             }); | ||||
| 
 | ||||
|         const cancelButton = Translations.t.general.cancel.Clone().SetClass("bg-white pl-4 pr-4").SetStyle("border-bottom-left-radius:30rem; border-bottom-right-radius: 30rem;"); | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ import FileSelectorButton from "../Input/FileSelectorButton"; | |||
| import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; | ||||
| import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; | ||||
| import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| export class ImageUploadFlow extends Toggle { | ||||
| 
 | ||||
|  | @ -28,7 +29,10 @@ export class ImageUploadFlow extends Toggle { | |||
|                 key = imagePrefix + ":" + freeIndex; | ||||
|             } | ||||
|             console.log("Adding image:" + key, url); | ||||
|             State.state.changes.addTag(tags.id, new Tag(key, url), tagsSource); | ||||
|             State.state.changes | ||||
|                 .applyAction(new ChangeTagAction( | ||||
|                     tags.id, new Tag(key, url), tagsSource.data | ||||
|                 )) | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|  |  | |||
|  | @ -42,7 +42,6 @@ export default class LocationInput extends InputElement<Loc> { | |||
|             } | ||||
|         ) | ||||
|         map.leafletMap.addCallbackAndRunD(leaflet => { | ||||
|             console.log(leaflet.getBounds(), leaflet.getBounds().pad(0.15)) | ||||
|             leaflet.setMaxBounds( | ||||
|                 leaflet.getBounds().pad(0.15) | ||||
|             ) | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import State from "../../State"; | |||
| import Toggle from "../Input/Toggle"; | ||||
| import Translations from "../i18n/Translations"; | ||||
| import Svg from "../../Svg"; | ||||
| import DeleteAction from "../../Logic/Osm/DeleteAction"; | ||||
| import DeleteAction from "../../Logic/Osm/Actions/DeleteAction"; | ||||
| import {Tag} from "../../Logic/Tags/Tag"; | ||||
| import {UIEventSource} from "../../Logic/UIEventSource"; | ||||
| import {TagsFilter} from "../../Logic/Tags/TagsFilter"; | ||||
|  | @ -19,6 +19,7 @@ import {Changes} from "../../Logic/Osm/Changes"; | |||
| import {And} from "../../Logic/Tags/And"; | ||||
| import Constants from "../../Models/Constants"; | ||||
| import DeleteConfig from "../../Customizations/JSON/DeleteConfig"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| export default class DeleteWizard extends Toggle { | ||||
|     /** | ||||
|  | @ -58,7 +59,9 @@ export default class DeleteWizard extends Toggle { | |||
|                 }) | ||||
|             } | ||||
|             (State.state?.changes ?? new Changes()) | ||||
|                 .addTag(id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource); | ||||
|                 .applyAction(new ChangeTagAction( | ||||
|                    id, new And(tagsToApply.map(kv => new Tag(kv.k, kv.v))), tagsSource.data | ||||
|                 )) | ||||
|         } | ||||
| 
 | ||||
|         function doDelete(selected: TagsFilter) { | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ import SharedTagRenderings from "../../Customizations/SharedTagRenderings"; | |||
| import BaseUIElement from "../BaseUIElement"; | ||||
| import {VariableUiElement} from "../Base/VariableUIElement"; | ||||
| import DeleteWizard from "./DeleteWizard"; | ||||
| import SplitRoadWizard from "./SplitRoadWizard"; | ||||
| 
 | ||||
| export default class FeatureInfoBox extends ScrollableFullScreen { | ||||
| 
 | ||||
|  | @ -66,10 +67,6 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|             renderings.push(questionBox); | ||||
|         } | ||||
| 
 | ||||
|         const hasMinimap = layerConfig.tagRenderings.some(tr => tr.hasMinimap()) | ||||
|         if (!hasMinimap) { | ||||
|             renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) | ||||
|         } | ||||
| 
 | ||||
|         if (layerConfig.deletion) { | ||||
|             renderings.push( | ||||
|  | @ -81,6 +78,19 @@ export default class FeatureInfoBox extends ScrollableFullScreen { | |||
|                 )) | ||||
|         } | ||||
| 
 | ||||
|         if (layerConfig.allowSplit) { | ||||
|             renderings.push( | ||||
|                 new VariableUiElement(tags.map(tags => tags.id).map(id => | ||||
|                     new SplitRoadWizard(id)) | ||||
|                 )) | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const hasMinimap = layerConfig.tagRenderings.some(tr => tr.hasMinimap()) | ||||
|         if (!hasMinimap) { | ||||
|             renderings.push(new TagRenderingAnswer(tags, SharedTagRenderings.SharedTagRendering.get("minimap"))) | ||||
|         } | ||||
| 
 | ||||
|         renderings.push( | ||||
|             new VariableUiElement( | ||||
|                 State.state.osmConnection.userDetails | ||||
|  |  | |||
							
								
								
									
										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 {Unit} from "../../Customizations/JSON/Denomination"; | ||||
| import InputElementWrapper from "../Input/InputElementWrapper"; | ||||
| import ChangeTagAction from "../../Logic/Osm/Actions/ChangeTagAction"; | ||||
| 
 | ||||
| /** | ||||
|  * Shows the question element. | ||||
|  | @ -56,7 +57,9 @@ export default class TagRenderingQuestion extends Combine { | |||
|             const selection = inputElement.GetValue().data; | ||||
|             if (selection) { | ||||
|                 (State.state?.changes ?? new Changes()) | ||||
|                     .addTag(tags.data.id, selection, tags); | ||||
|                     .applyAction(new ChangeTagAction( | ||||
|                         tags.data.id, selection, tags.data | ||||
|                     )) | ||||
|             } | ||||
| 
 | ||||
|             if (options.afterSave) { | ||||
|  | @ -195,9 +198,7 @@ export default class TagRenderingQuestion extends Combine { | |||
|                     oppositeTags.push(notSelected); | ||||
|                 } | ||||
|                 tags.push(TagUtils.FlattenMultiAnswer(oppositeTags)); | ||||
|                 const actualTags = TagUtils.FlattenMultiAnswer(tags); | ||||
|                 console.log("Converted ", indices.join(","), "into", actualTags.asHumanString(false, false, {}), "with elems", elements) | ||||
|                 return actualTags; | ||||
|                 return TagUtils.FlattenMultiAnswer(tags); | ||||
|             }, | ||||
|             (tags: TagsFilter) => { | ||||
|                 // {key --> values[]}
 | ||||
|  |  | |||
|  | @ -22,7 +22,8 @@ export default class ShowDataLayer { | |||
|                 leafletMap: UIEventSource<L.Map>, | ||||
|                 layoutToUse: UIEventSource<LayoutConfig>, | ||||
|                 enablePopups = true, | ||||
|                 zoomToFeatures = false) { | ||||
|                 zoomToFeatures = false, | ||||
|                 name?: string) { | ||||
|         this._leafletMap = leafletMap; | ||||
|         this._enablePopups = enablePopups; | ||||
|         this._features = features; | ||||
|  | @ -85,9 +86,7 @@ export default class ShowDataLayer { | |||
|                     console.error(e) | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
| 
 | ||||
|             State.state.selectedElement.ping(); | ||||
|             State.state.selectedElement.ping() | ||||
|         } | ||||
| 
 | ||||
|         features.addCallback(() => update()); | ||||
|  | @ -131,6 +130,7 @@ export default class ShowDataLayer { | |||
|             }) | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     private postProcessFeature(feature, leafletLayer: L.Layer) { | ||||
|         const layer: LayerConfig = this._layerDict[feature._matching_layer_id]; | ||||
|         if (layer === undefined) { | ||||
|  | @ -160,7 +160,7 @@ export default class ShowDataLayer { | |||
| 
 | ||||
|         leafletLayer.on("popupopen", () => { | ||||
|             State.state.selectedElement.setData(feature) | ||||
|             | ||||
| 
 | ||||
|             if (infobox === undefined) { | ||||
|                 const tags = State.state.allElements.getEventSourceById(feature.properties.id); | ||||
|                 infobox = new FeatureInfoBox(tags, layer); | ||||
|  | @ -175,7 +175,7 @@ export default class ShowDataLayer { | |||
| 
 | ||||
| 
 | ||||
|             infobox.AttachTo(id) | ||||
|             infobox.Activate();  | ||||
|             infobox.Activate(); | ||||
|         }); | ||||
|         const self = this; | ||||
|         State.state.selectedElement.addCallbackAndRunD(selected => { | ||||
|  | @ -188,11 +188,13 @@ export default class ShowDataLayer { | |||
|             if (selected.properties.id === feature.properties.id) { | ||||
|                 // 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
 | ||||
| 
 | ||||
|                     &&                     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() | ||||
|                 } | ||||
|                 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)) { | ||||
|                                 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;") | ||||
|                 }) | ||||
|             }, | ||||
|  | @ -127,6 +130,7 @@ export default class SpecialVisualizations { | |||
|                                 // This is a list of values
 | ||||
|                                 idList = JSON.parse(value) | ||||
|                             } | ||||
|                              | ||||
|                             for (const id of idList) { | ||||
|                                 features.push({ | ||||
|                                     freshness: new Date(), | ||||
|  |  | |||
|  | @ -19,7 +19,6 @@ export class SubstitutedTranslation extends VariableUiElement { | |||
|         const extraMappings: SpecialVisualization[] = []; | ||||
| 
 | ||||
|         mapping?.forEach((value, key) => { | ||||
|             console.log("KV:", key, value) | ||||
|             extraMappings.push( | ||||
|                 { | ||||
|                     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)) { | ||||
| 
 | ||||
|             // 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
 | ||||
|                     const date: Date = el; | ||||
|                     rtext = date.toLocaleString(); | ||||
|                 } else if (el.ConstructElement() === undefined) { | ||||
|                     console.error("InnerREnder is not defined", el); | ||||
|                     throw "Hmmm, el.InnerRender is not defined?" | ||||
|                 } else if (el.ConstructElement === undefined) { | ||||
|                     console.error("ConstructElement is not defined", el); | ||||
|                     throw "ConstructElement is not defined, you are working with a "+(typeof el)+":"+(el.constructor.name) | ||||
|                 } else { | ||||
|                     Translation.forcedLanguage = lang; // This is a very dirty hack - it'll bite me one day
 | ||||
|                     rtext = el.ConstructElement().innerHTML; | ||||
|  |  | |||
							
								
								
									
										15
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										15
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -135,7 +135,20 @@ export class Utils { | |||
|         } | ||||
|         return newArr; | ||||
|     } | ||||
| 
 | ||||
|      | ||||
|     public static Identical<T>(t1: T[], t2: T[], eq?: (t: T, t0: T) => boolean): boolean{ | ||||
|         if(t1.length !== t2.length){ | ||||
|             return false | ||||
|         } | ||||
|         eq = (a, b) => a === b | ||||
|         for (let i = 0; i < t1.length ; i++) { | ||||
|             if(!eq(t1[i] ,t2[i])){ | ||||
|                 return false | ||||
|             } | ||||
|         } | ||||
|         return true; | ||||
|     } | ||||
|      | ||||
|     public static MergeTags(a: any, b: any) { | ||||
|         const t = {}; | ||||
|         for (const k in a) { | ||||
|  |  | |||
|  | @ -1302,5 +1302,15 @@ | |||
|     "sources": [ | ||||
|       "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, | ||||
|   "startLon": 3.2228, | ||||
|   "maintainer": "MapComplete", | ||||
|   "widenfactor": 0.05, | ||||
|   "widenfactor": 0.01, | ||||
|   "roamingRenderings": [ | ||||
|     { | ||||
|       "question": { | ||||
|  | @ -276,7 +276,8 @@ | |||
|       }, | ||||
|       "tagRenderings": [ | ||||
|         "images" | ||||
|       ] | ||||
|       ], | ||||
|       "allowSplit": true | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|  | @ -27,6 +27,14 @@ | |||
|     "intro": "MapComplete is an OpenStreetMap-viewer and editor, which shows you information about a specific theme.", | ||||
|     "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", | ||||
|     "cancel": "Cancel", | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|   "main": "index.js", | ||||
|   "scripts": { | ||||
|     "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", | ||||
|     "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", | ||||
|  |  | |||
							
								
								
									
										14
									
								
								test.html
									
										
									
									
									
								
							
							
						
						
									
										14
									
								
								test.html
									
										
									
									
									
								
							|  | @ -4,10 +4,16 @@ | |||
|     <title>Small tests</title> | ||||
|     <link href="index.css" rel="stylesheet"/> | ||||
|     <link rel="stylesheet" href="./vendor/leaflet.css"/> | ||||
|     <link href="css/tabbedComponent.css" rel="stylesheet"/> | ||||
|     <link href="css/openinghourstable.css" rel="stylesheet"/> | ||||
|     <link href="css/tagrendering.css" rel="stylesheet"/> | ||||
|     <link href="css/ReviewElement.css" rel="stylesheet"/> | ||||
|     <link rel="stylesheet" href="./index.css"/> | ||||
|     <link rel="stylesheet" href="./css/userbadge.css"/> | ||||
|     <link rel="stylesheet" href="./css/tabbedComponent.css"/> | ||||
|     <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"> | ||||
| 
 | ||||
| </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