forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			162 lines
		
	
	
		
			No EOL
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			162 lines
		
	
	
		
			No EOL
		
	
	
		
			5.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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
 | |
|     }
 | |
| } |