forked from MapComplete/MapComplete
		
	Refactoring: fix rendering of new roads, generated by a split
This commit is contained in:
		
							parent
							
								
									840990c08b
								
							
						
					
					
						commit
						8eb2c68f79
					
				
					 34 changed files with 443 additions and 333 deletions
				
			
		|  | @ -1,6 +1,9 @@ | |||
| import { Changes } from "../Osm/Changes" | ||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore"; | ||||
| import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesStore" | ||||
| 
 | ||||
| /** | ||||
|  * Applies tag changes onto the featureStore | ||||
|  */ | ||||
| export default class ChangeToElementsActor { | ||||
|     constructor(changes: Changes, allElements: FeaturePropertiesStore) { | ||||
|         changes.pendingChanges.addCallbackAndRun((changes) => { | ||||
|  |  | |||
|  | @ -55,12 +55,13 @@ class SingleTileSaver { | |||
|  */ | ||||
| export default class SaveFeatureSourceToLocalStorage { | ||||
|     constructor( | ||||
|         backend: string, | ||||
|         layername: string, | ||||
|         zoomlevel: number, | ||||
|         features: FeatureSource, | ||||
|         featureProperties: FeaturePropertiesStore | ||||
|     ) { | ||||
|         const storage = TileLocalStorage.construct<Feature[]>(layername) | ||||
|         const storage = TileLocalStorage.construct<Feature[]>(backend, layername) | ||||
|         const singleTileSavers: Map<number, SingleTileSaver> = new Map<number, SingleTileSaver>() | ||||
|         features.features.addCallbackAndRunD((features) => { | ||||
|             const sliced = GeoOperations.slice(zoomlevel, features) | ||||
|  |  | |||
|  | @ -17,14 +17,15 @@ export default class TileLocalStorage<T> { | |||
|         this._layername = layername | ||||
|     } | ||||
| 
 | ||||
|     public static construct<T>(layername: string): TileLocalStorage<T> { | ||||
|         const cached = TileLocalStorage.perLayer[layername] | ||||
|     public static construct<T>(backend: string, layername: string): TileLocalStorage<T> { | ||||
|         const key = backend + "_" + layername | ||||
|         const cached = TileLocalStorage.perLayer[key] | ||||
|         if (cached) { | ||||
|             return cached | ||||
|         } | ||||
| 
 | ||||
|         const tls = new TileLocalStorage<T>(layername) | ||||
|         TileLocalStorage.perLayer[layername] = tls | ||||
|         const tls = new TileLocalStorage<T>(key) | ||||
|         TileLocalStorage.perLayer[key] = tls | ||||
|         return tls | ||||
|     } | ||||
| 
 | ||||
|  | @ -46,7 +47,7 @@ export default class TileLocalStorage<T> { | |||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     private async SetIdb(tileIndex: number, data): Promise<void> { | ||||
|     private async SetIdb(tileIndex: number, data: any): Promise<void> { | ||||
|         try { | ||||
|             await IdbLocalStorage.SetDirectly(this._layername + "_" + tileIndex, data) | ||||
|         } catch (e) { | ||||
|  |  | |||
|  | @ -3,22 +3,18 @@ | |||
|  */ | ||||
| import { Changes } from "../../Osm/Changes" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { FeatureSourceForLayer, IndexedFeatureSource } from "../FeatureSource" | ||||
| import FilteredLayer from "../../../Models/FilteredLayer" | ||||
| import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" | ||||
| import { ChangeDescription, ChangeDescriptionTools } from "../../Osm/Actions/ChangeDescription" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | ||||
|     public readonly features: UIEventSource<Feature[]> = | ||||
|         new UIEventSource<Feature[]>([]) | ||||
|     public readonly layer: FilteredLayer | ||||
| export default class ChangeGeometryApplicator implements FeatureSource { | ||||
|     public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) | ||||
|     private readonly source: IndexedFeatureSource | ||||
|     private readonly changes: Changes | ||||
| 
 | ||||
|     constructor(source: IndexedFeatureSource & FeatureSourceForLayer, changes: Changes) { | ||||
|     constructor(source: IndexedFeatureSource, changes: Changes) { | ||||
|         this.source = source | ||||
|         this.changes = changes | ||||
|         this.layer = source.layer | ||||
| 
 | ||||
|         this.features = new UIEventSource<Feature[]>(undefined) | ||||
| 
 | ||||
|  | @ -30,10 +26,10 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | |||
| 
 | ||||
|     private update() { | ||||
|         const upstreamFeatures = this.source.features.data | ||||
|         const upstreamIds = this.source.containedIds.data | ||||
|         const upstreamIds = this.source.featuresById.data | ||||
|         const changesToApply = this.changes.allChanges.data?.filter( | ||||
|             (ch) => | ||||
|                 // Does upsteram have this element? If not, we skip
 | ||||
|                 // Does upstream have this element? If not, we skip
 | ||||
|                 upstreamIds.has(ch.type + "/" + ch.id) && | ||||
|                 // Are any (geometry) changes defined?
 | ||||
|                 ch.changes !== undefined && | ||||
|  | @ -61,7 +57,7 @@ export default class ChangeGeometryApplicator implements FeatureSourceForLayer { | |||
|         for (const feature of upstreamFeatures) { | ||||
|             const changesForFeature = changesPerId.get(feature.properties.id) | ||||
|             if (changesForFeature === undefined) { | ||||
|                 // No changes for this element
 | ||||
|                 // No changes for this element - simply pass it along to downstream
 | ||||
|                 newFeatures.push(feature) | ||||
|                 continue | ||||
|             } | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| import { Store, UIEventSource } from "../../UIEventSource" | ||||
| import { FeatureSource ,  IndexedFeatureSource } from "../FeatureSource" | ||||
| import { FeatureSource, IndexedFeatureSource } from "../FeatureSource" | ||||
| import { Feature } from "geojson" | ||||
| import { Utils } from "../../../Utils" | ||||
| 
 | ||||
|  | @ -19,6 +19,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { | |||
|         this._featuresById = new UIEventSource<Map<string, Feature>>(undefined) | ||||
|         this.featuresById = this._featuresById | ||||
|         const self = this | ||||
|         sources = Utils.NoNull(sources) | ||||
|         for (let source of sources) { | ||||
|             source.features.addCallback(() => { | ||||
|                 self.addData(sources.map((s) => s.features.data)) | ||||
|  | @ -28,7 +29,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource { | |||
|         this._sources = sources | ||||
|     } | ||||
| 
 | ||||
|     protected addSource(source: FeatureSource) { | ||||
|     public addSource(source: FeatureSource) { | ||||
|         this._sources.push(source) | ||||
|         source.features.addCallbackAndRun(() => { | ||||
|             this.addData(this._sources.map((s) => s.features.data)) | ||||
|  |  | |||
|  | @ -4,13 +4,14 @@ import { FeatureSource } from "../FeatureSource" | |||
| import { Or } from "../../Tags/Or" | ||||
| import FeatureSwitchState from "../../State/FeatureSwitchState" | ||||
| import OverpassFeatureSource from "./OverpassFeatureSource" | ||||
| import { ImmutableStore, Store } from "../../UIEventSource" | ||||
| import { ImmutableStore, Store, UIEventSource } from "../../UIEventSource" | ||||
| import OsmFeatureSource from "./OsmFeatureSource" | ||||
| import FeatureSourceMerger from "./FeatureSourceMerger" | ||||
| import DynamicGeoJsonTileSource from "../TiledFeatureSource/DynamicGeoJsonTileSource" | ||||
| import { BBox } from "../../BBox" | ||||
| import LocalStorageFeatureSource from "../TiledFeatureSource/LocalStorageFeatureSource" | ||||
| import StaticFeatureSource from "./StaticFeatureSource" | ||||
| import { OsmPreferences } from "../../Osm/OsmPreferences" | ||||
| 
 | ||||
| /** | ||||
|  * This source will fetch the needed data from various sources for the given layout. | ||||
|  | @ -18,15 +19,14 @@ import StaticFeatureSource from "./StaticFeatureSource" | |||
|  * Note that special layers (with `source=null` will be ignored) | ||||
|  */ | ||||
| export default class LayoutSource extends FeatureSourceMerger { | ||||
|     private readonly _isLoading: UIEventSource<boolean> = new UIEventSource<boolean>(false) | ||||
|     /** | ||||
|      * Indicates if a data source is loading something | ||||
|      * TODO fixme | ||||
|      */ | ||||
|     public readonly isLoading: Store<boolean> = new ImmutableStore(false) | ||||
|     public readonly isLoading: Store<boolean> = this._isLoading | ||||
|     constructor( | ||||
|         layers: LayerConfig[], | ||||
|         featureSwitches: FeatureSwitchState, | ||||
|         newAndChangedElements: FeatureSource, | ||||
|         mapProperties: { bounds: Store<BBox>; zoom: Store<number> }, | ||||
|         backend: string, | ||||
|         isDisplayed: (id: string) => Store<boolean> | ||||
|  | @ -39,7 +39,7 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         const osmLayers = layers.filter((layer) => layer.source.geojsonSource === undefined) | ||||
|         const fromCache = osmLayers.map( | ||||
|             (l) => | ||||
|                 new LocalStorageFeatureSource(l.id, 15, mapProperties, { | ||||
|                 new LocalStorageFeatureSource(backend, l.id, 15, mapProperties, { | ||||
|                     isActive: isDisplayed(l.id), | ||||
|                 }) | ||||
|         ) | ||||
|  | @ -56,7 +56,17 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         ) | ||||
| 
 | ||||
|         const expiryInSeconds = Math.min(...(layers?.map((l) => l.maxAgeOfCache) ?? [])) | ||||
|         super(overpassSource, osmApiSource, newAndChangedElements, ...geojsonSources, ...fromCache) | ||||
| 
 | ||||
|         super(overpassSource, osmApiSource, ...geojsonSources, ...fromCache) | ||||
| 
 | ||||
|         const self = this | ||||
|         function setIsLoading() { | ||||
|             const loading = overpassSource?.runningQuery?.data && osmApiSource?.isRunning?.data | ||||
|             self._isLoading.setData(loading) | ||||
|         } | ||||
| 
 | ||||
|         overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading()) | ||||
|         osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading()) | ||||
|     } | ||||
| 
 | ||||
|     private static setupGeojsonSource( | ||||
|  | @ -83,9 +93,9 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         zoom: Store<number>, | ||||
|         backend: string, | ||||
|         featureSwitches: FeatureSwitchState | ||||
|     ): FeatureSource { | ||||
|     ): OsmFeatureSource | undefined { | ||||
|         if (osmLayers.length == 0) { | ||||
|             return new StaticFeatureSource(new ImmutableStore([])) | ||||
|             return undefined | ||||
|         } | ||||
|         const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) | ||||
|         const isActive = zoom.mapD((z) => { | ||||
|  | @ -115,9 +125,9 @@ export default class LayoutSource extends FeatureSourceMerger { | |||
|         bounds: Store<BBox>, | ||||
|         zoom: Store<number>, | ||||
|         featureSwitches: FeatureSwitchState | ||||
|     ): FeatureSource { | ||||
|     ): OverpassFeatureSource | undefined { | ||||
|         if (osmLayers.length == 0) { | ||||
|             return new StaticFeatureSource(new ImmutableStore([])) | ||||
|             return undefined | ||||
|         } | ||||
|         const minzoom = Math.min(...osmLayers.map((layer) => layer.minzoom)) | ||||
|         const isActive = zoom.mapD((z) => { | ||||
|  |  | |||
|  | @ -1,13 +1,12 @@ | |||
| import { Changes } from "../../Osm/Changes" | ||||
| import { OsmNode, OsmObject, OsmRelation, OsmWay } from "../../Osm/OsmObject" | ||||
| import { FeatureSource } from "../FeatureSource" | ||||
| import { IndexedFeatureSource, WritableFeatureSource } from "../FeatureSource" | ||||
| import { UIEventSource } from "../../UIEventSource" | ||||
| import { ChangeDescription } from "../../Osm/Actions/ChangeDescription" | ||||
| import { ElementStorage } from "../../ElementStorage" | ||||
| import { OsmId, OsmTags } from "../../../Models/OsmFeature" | ||||
| import { Feature } from "geojson" | ||||
| 
 | ||||
| export class NewGeometryFromChangesFeatureSource implements FeatureSource { | ||||
| export class NewGeometryFromChangesFeatureSource implements WritableFeatureSource { | ||||
|     // This class name truly puts the 'Java' into 'Javascript'
 | ||||
| 
 | ||||
|     /** | ||||
|  | @ -18,7 +17,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | |||
|      */ | ||||
|     public readonly features: UIEventSource<Feature[]> = new UIEventSource<Feature[]>([]) | ||||
| 
 | ||||
|     constructor(changes: Changes, allElementStorage: ElementStorage, backendUrl: string) { | ||||
|     constructor(changes: Changes, allElementStorage: IndexedFeatureSource, backendUrl: string) { | ||||
|         const seenChanges = new Set<ChangeDescription>() | ||||
|         const features = this.features.data | ||||
|         const self = this | ||||
|  | @ -53,7 +52,7 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | |||
|                     // In _most_ of the cases, this means that this _isn't_ a new object
 | ||||
|                     // However, when a point is snapped to an already existing point, we have to create a representation for this point!
 | ||||
|                     // For this, we introspect the change
 | ||||
|                     if (allElementStorage.has(change.type + "/" + change.id)) { | ||||
|                     if (allElementStorage.featuresById.data.has(change.type + "/" + change.id)) { | ||||
|                         // The current point already exists, we don't have to do anything here
 | ||||
|                         continue | ||||
|                     } | ||||
|  | @ -65,7 +64,6 @@ export class NewGeometryFromChangesFeatureSource implements FeatureSource { | |||
|                             feat.tags[kv.k] = kv.v | ||||
|                         } | ||||
|                         const geojson = feat.asGeoJson() | ||||
|                         allElementStorage.addOrGetElement(geojson) | ||||
|                         self.features.data.push(geojson) | ||||
|                         self.features.ping() | ||||
|                     }) | ||||
|  |  | |||
|  | @ -41,7 +41,6 @@ export default class OsmFeatureSource extends FeatureSourceMerger { | |||
|         this.isActive = options.isActive ?? new ImmutableStore(true) | ||||
|         this._backend = options.backend ?? "https://www.openstreetmap.org" | ||||
|         this._bounds.addCallbackAndRunD((bbox) => this.loadData(bbox)) | ||||
|         console.log("Allowed tags are:", this.allowedTags) | ||||
|     } | ||||
| 
 | ||||
|     private async loadData(bbox: BBox) { | ||||
|  | @ -108,7 +107,7 @@ export default class OsmFeatureSource extends FeatureSourceMerger { | |||
|     } | ||||
| 
 | ||||
|     private async LoadTile(z, x, y): Promise<void> { | ||||
|         console.log("OsmFeatureSource: loading ", z, x, y) | ||||
|         console.log("OsmFeatureSource: loading ", z, x, y, "from", this._backend) | ||||
|         if (z >= 22) { | ||||
|             throw "This is an absurd high zoom level" | ||||
|         } | ||||
|  |  | |||
|  | @ -22,13 +22,18 @@ export interface SnappingOptions { | |||
|      * The resulting snap coordinates will be written into this UIEventSource | ||||
|      */ | ||||
|     snapLocation?: UIEventSource<{ lon: number; lat: number }> | ||||
| 
 | ||||
|     /** | ||||
|      * If the projected point is within `reusePointWithin`-meter of an already existing point | ||||
|      */ | ||||
|     reusePointWithin?: number | ||||
| } | ||||
| 
 | ||||
| export default class SnappingFeatureSource implements FeatureSource { | ||||
|     public readonly features: Store<Feature<Point>[]> | ||||
| 
 | ||||
|     private readonly _snappedTo: UIEventSource<string> | ||||
|     /*Contains the id of the way it snapped to*/ | ||||
|     public readonly snappedTo: Store<string> | ||||
|     private readonly _snappedTo: UIEventSource<string> | ||||
| 
 | ||||
|     constructor( | ||||
|         snapTo: FeatureSource, | ||||
|  |  | |||
|  | @ -7,6 +7,7 @@ import StaticFeatureSource from "../Sources/StaticFeatureSource" | |||
| 
 | ||||
| export default class LocalStorageFeatureSource extends DynamicTileSource { | ||||
|     constructor( | ||||
|         backend: string, | ||||
|         layername: string, | ||||
|         zoomlevel: number, | ||||
|         mapProperties: { | ||||
|  | @ -17,7 +18,7 @@ export default class LocalStorageFeatureSource extends DynamicTileSource { | |||
|             isActive?: Store<boolean> | ||||
|         } | ||||
|     ) { | ||||
|         const storage = TileLocalStorage.construct<Feature[]>(layername) | ||||
|         const storage = TileLocalStorage.construct<Feature[]>(backend, layername) | ||||
|         super( | ||||
|             zoomlevel, | ||||
|             (tileIndex) => | ||||
|  |  | |||
|  | @ -294,6 +294,10 @@ export class GeoOperations { | |||
|      * Mostly used as helper for 'nearestPoint' | ||||
|      * @param way | ||||
|      */ | ||||
|     public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString> | ||||
|     public static forceLineString( | ||||
|         way: Feature<MultiLineString | MultiPolygon> | ||||
|     ): Feature<MultiLineString> | ||||
|     public static forceLineString( | ||||
|         way: Feature<LineString | MultiLineString | Polygon | MultiPolygon> | ||||
|     ): Feature<LineString | MultiLineString> { | ||||
|  | @ -972,4 +976,9 @@ export class GeoOperations { | |||
|             }, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     static centerpointCoordinatesObj(geojson: Feature) { | ||||
|         const [lon, lat] = GeoOperations.centerpointCoordinates(geojson) | ||||
|         return { lon, lat } | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -4,6 +4,7 @@ import { GeoOperations } from "../../GeoOperations" | |||
| import OsmChangeAction from "./OsmChangeAction" | ||||
| import { ChangeDescription } from "./ChangeDescription" | ||||
| import RelationSplitHandler from "./RelationSplitHandler" | ||||
| import { Feature, LineString } from "geojson" | ||||
| 
 | ||||
| interface SplitInfo { | ||||
|     originalIndex?: number // or negative for new elements
 | ||||
|  | @ -14,9 +15,9 @@ interface SplitInfo { | |||
| export default class SplitAction extends OsmChangeAction { | ||||
|     private readonly wayId: string | ||||
|     private readonly _splitPointsCoordinates: [number, number][] // lon, lat
 | ||||
|     private _meta: { theme: string; changeType: "split" } | ||||
|     private _toleranceInMeters: number | ||||
|     private _withNewCoordinates: (coordinates: [number, number][]) => void | ||||
|     private readonly _meta: { theme: string; changeType: "split" } | ||||
|     private readonly _toleranceInMeters: number | ||||
|     private readonly _withNewCoordinates: (coordinates: [number, number][]) => void | ||||
| 
 | ||||
|     /** | ||||
|      * Create a changedescription for splitting a point. | ||||
|  | @ -197,7 +198,7 @@ export default class SplitAction extends OsmChangeAction { | |||
|      * If another point is closer then ~5m, we reuse that point | ||||
|      */ | ||||
|     private CalculateSplitCoordinates(osmWay: OsmWay, toleranceInM = 5): SplitInfo[] { | ||||
|         const wayGeoJson = osmWay.asGeoJson() | ||||
|         const wayGeoJson = <Feature<LineString>>osmWay.asGeoJson() | ||||
|         // Should be [lon, lat][]
 | ||||
|         const originalPoints: [number, number][] = osmWay.coordinates.map((c) => [c[1], c[0]]) | ||||
|         const allPoints: { | ||||
|  |  | |||
|  | @ -18,10 +18,6 @@ import FeaturePropertiesStore from "../FeatureSource/Actors/FeaturePropertiesSto | |||
|  * Needs an authenticator via OsmConnection | ||||
|  */ | ||||
| export class Changes { | ||||
|     /** | ||||
|      * All the newly created features as featureSource + all the modified features | ||||
|      */ | ||||
|     public readonly features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||
|     public readonly pendingChanges: UIEventSource<ChangeDescription[]> = | ||||
|         LocalStorageSource.GetParsed<ChangeDescription[]>("pending-changes", []) | ||||
|     public readonly allChanges = new UIEventSource<ChangeDescription[]>(undefined) | ||||
|  |  | |||
|  | @ -213,7 +213,7 @@ class RewriteMetaInfoTags extends SimpleMetaTagger { | |||
|         move("changeset", "_last_edit:changeset") | ||||
|         move("timestamp", "_last_edit:timestamp") | ||||
|         move("version", "_version_number") | ||||
|         feature.properties._backend = "https://openstreetmap.org" | ||||
|         feature.properties._backend = feature.properties._backend ?? "https://openstreetmap.org" | ||||
|         return movedSomething | ||||
|     } | ||||
| } | ||||
|  |  | |||
|  | @ -38,8 +38,9 @@ export class IdbLocalStorage { | |||
|         return src | ||||
|     } | ||||
| 
 | ||||
|     public static SetDirectly(key: string, value): Promise<void> { | ||||
|         return idb.set(key, value) | ||||
|     public static SetDirectly(key: string, value: any): Promise<void> { | ||||
|         const copy = Utils.Clone(value) | ||||
|         return idb.set(key, copy) | ||||
|     } | ||||
| 
 | ||||
|     static GetDirectly(key: string): Promise<void> { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue