forked from MapComplete/MapComplete
		
	First working version of fully automatic uploader
This commit is contained in:
		
							parent
							
								
									04dc373b1e
								
							
						
					
					
						commit
						e922768f99
					
				
					 21 changed files with 342 additions and 106 deletions
				
			
		|  | @ -119,7 +119,11 @@ export default class SaveTileToLocalStorageActor { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private SetIdb(tileIndex, data) { |     private SetIdb(tileIndex, data) { | ||||||
|         IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) |         try{ | ||||||
|  |             IdbLocalStorage.SetDirectly(this._layer.id + "_" + tileIndex, data) | ||||||
|  |         }catch(e){ | ||||||
|  |             console.error("Could not save tile to indexed-db: ", e, "tileIndex is:", tileIndex, "for layer", this._layer.id) | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private GetIdb(tileIndex) { |     private GetIdb(tileIndex) { | ||||||
|  |  | ||||||
|  | @ -171,6 +171,7 @@ export default class FeaturePipeline { | ||||||
|                     state.currentBounds, state.locationControl, |                     state.currentBounds, state.locationControl, | ||||||
|                     (tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness), |                     (tileIndex, freshness) => self.freshnesses.get(id).addTileLoad(tileIndex, freshness), | ||||||
|                     (tile) => { |                     (tile) => { | ||||||
|  |                         console.debug("Loaded tile ", id, tile.tileIndex, "from local cache") | ||||||
|                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) |                         new RegisteringAllFromFeatureSourceActor(tile, state.allElements) | ||||||
|                         hierarchy.registerTile(tile); |                         hierarchy.registerTile(tile); | ||||||
|                         tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) |                         tile.features.addCallbackAndRunD(_ => self.newDataLoadedSignal.setData(tile)) | ||||||
|  | @ -247,6 +248,7 @@ export default class FeaturePipeline { | ||||||
|                 }) |                 }) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  |          | ||||||
|         if (state.layoutToUse.trackAllNodes) { |         if (state.layoutToUse.trackAllNodes) { | ||||||
|             const fullNodeDb = new FullNodeDatabaseSource( |             const fullNodeDb = new FullNodeDatabaseSource( | ||||||
|                 state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0], |                 state.filteredLayers.data.filter(l => l.layerDef.id === "type_node")[0], | ||||||
|  | @ -289,6 +291,10 @@ export default class FeaturePipeline { | ||||||
|         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 |         // A NewGeometryFromChangesFeatureSource does not split per layer, so we do this next
 | ||||||
|         new PerLayerFeatureSourceSplitter(state.filteredLayers, |         new PerLayerFeatureSourceSplitter(state.filteredLayers, | ||||||
|             (perLayer) => { |             (perLayer) => { | ||||||
|  |                 if(perLayer.features.data.length === 0){ | ||||||
|  |                     return | ||||||
|  |                 } | ||||||
|  |              | ||||||
|                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 |                 // We don't bother to split them over tiles as it'll contain little features by default, so we simply add them like this
 | ||||||
|                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) |                 perLayerHierarchy.get(perLayer.layer.layerDef.id).registerTile(perLayer) | ||||||
|                 // AT last, we always apply the metatags whenever possible
 |                 // AT last, we always apply the metatags whenever possible
 | ||||||
|  | @ -309,7 +315,7 @@ export default class FeaturePipeline { | ||||||
| 
 | 
 | ||||||
|         this.runningQuery = updater.runningQuery.map( |         this.runningQuery = updater.runningQuery.map( | ||||||
|             overpass => { |             overpass => { | ||||||
|                 console.log("FeaturePipeline: runningQuery state changed. Overpass", overpass ? "is querying," : "is idle,", |                 console.log("FeaturePipeline: runningQuery state changed: Overpass", overpass ? "is querying," : "is idle,", | ||||||
|                     "osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle") |                     "osmFeatureSource is", osmFeatureSource.isRunning ? "is running and needs " + neededTilesFromOsm.data?.length + " tiles (already got " + osmFeatureSource.downloadedTiles.size + " tiles )" : "is idle") | ||||||
|                 return overpass || osmFeatureSource.isRunning.data; |                 return overpass || osmFeatureSource.isRunning.data; | ||||||
|             }, [osmFeatureSource.isRunning] |             }, [osmFeatureSource.isRunning] | ||||||
|  | @ -355,7 +361,15 @@ export default class FeaturePipeline { | ||||||
|             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { |             if (this.state.locationControl.data.zoom < flayer.layerDef.minzoom) { | ||||||
|                 continue; |                 continue; | ||||||
|             } |             } | ||||||
|             const freshness = this.freshnesses.get(flayer.layerDef.id).freshnessFor(z, x, y) |             if(flayer.layerDef.maxAgeOfCache === 0){ | ||||||
|  |                 return undefined; | ||||||
|  |             } | ||||||
|  |             const freshnessCalc = this.freshnesses.get(flayer.layerDef.id) | ||||||
|  |             if(freshnessCalc === undefined){ | ||||||
|  |                 console.warn("No freshness tracker found for ", flayer.layerDef.id) | ||||||
|  |                 return undefined | ||||||
|  |             } | ||||||
|  |             const freshness = freshnessCalc.freshnessFor(z, x, y) | ||||||
|             if (freshness === undefined) { |             if (freshness === undefined) { | ||||||
|                 // SOmething is undefined --> we return undefined as we have to download
 |                 // SOmething is undefined --> we return undefined as we have to download
 | ||||||
|                 return undefined |                 return undefined | ||||||
|  | @ -409,11 +423,13 @@ export default class FeaturePipeline { | ||||||
|         const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) |         const minzoom = Math.min(...state.layoutToUse.layers.map(layer => layer.minzoom)) | ||||||
|         const overpassIsActive = state.currentBounds.map(bbox => { |         const overpassIsActive = state.currentBounds.map(bbox => { | ||||||
|             if (bbox === undefined) { |             if (bbox === undefined) { | ||||||
|  |                 console.debug("Disabling overpass source: no bbox") | ||||||
|                 return false |                 return false | ||||||
|             } |             } | ||||||
|             let zoom = state.locationControl.data.zoom |             let zoom = state.locationControl.data.zoom | ||||||
|             if (zoom < minzoom) { |             if (zoom < minzoom) { | ||||||
|                 // We are zoomed out over the zoomlevel of any layer
 |                 // We are zoomed out over the zoomlevel of any layer
 | ||||||
|  |                 console.debug("Disabling overpass source: zoom < minzoom") | ||||||
|                 return false; |                 return false; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|  | @ -456,31 +472,26 @@ export default class FeaturePipeline { | ||||||
|         if(src === undefined){ |         if(src === undefined){ | ||||||
|             throw "Src is undefined" |             throw "Src is undefined" | ||||||
|         } |         } | ||||||
|         window.setTimeout( |         const layerDef = src.layer.layerDef; | ||||||
|             () => { |         MetaTagging.addMetatags( | ||||||
|                 const layerDef = src.layer.layerDef; |             src.features.data, | ||||||
|                 MetaTagging.addMetatags( |             { | ||||||
|                     src.features.data, |                 memberships: this.relationTracker, | ||||||
|                     { |                 getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox), | ||||||
|                         memberships: this.relationTracker, |                 getFeatureById: (id: string) => self.state.allElements.ContainingFeatures.get(id) | ||||||
|                         getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox), |  | ||||||
|                         getFeatureById: (id: string) => self.state.allElements.ContainingFeatures.get(id) |  | ||||||
|                     }, |  | ||||||
|                     layerDef, |  | ||||||
|                     state, |  | ||||||
|                     { |  | ||||||
|                         includeDates: true, |  | ||||||
|                         // We assume that the non-dated metatags are already set by the cache generator
 |  | ||||||
|                         includeNonDates: layerDef.source.geojsonSource === undefined || !layerDef.source.isOsmCacheLayer |  | ||||||
|                     } |  | ||||||
|                 ) |  | ||||||
|             }, |             }, | ||||||
|             15 |             layerDef, | ||||||
|  |             state, | ||||||
|  |             { | ||||||
|  |                 includeDates: true, | ||||||
|  |                 // We assume that the non-dated metatags are already set by the cache generator
 | ||||||
|  |                 includeNonDates: layerDef.source.geojsonSource === undefined || !layerDef.source.isOsmCacheLayer | ||||||
|  |             } | ||||||
|         ) |         ) | ||||||
| 
 | 
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private updateAllMetaTagging() { |     public updateAllMetaTagging() { | ||||||
|         const self = this; |         const self = this; | ||||||
|         console.debug("Updating the meta tagging of all tiles as new data got loaded") |         console.debug("Updating the meta tagging of all tiles as new data got loaded") | ||||||
|         this.perLayerHierarchy.forEach(hierarchy => { |         this.perLayerHierarchy.forEach(hierarchy => { | ||||||
|  |  | ||||||
|  | @ -36,21 +36,15 @@ export default class PerLayerFeatureSourceSplitter { | ||||||
|             const featuresPerLayer = new Map<string, { feature, freshness } []>(); |             const featuresPerLayer = new Map<string, { feature, freshness } []>(); | ||||||
|             const noLayerFound = [] |             const noLayerFound = [] | ||||||
| 
 | 
 | ||||||
|             function addTo(layer: FilteredLayer, feature: { feature, freshness }) { |             for (const layer of layers.data) { | ||||||
|                 const id = layer.layerDef.id |                 featuresPerLayer.set(layer.layerDef.id, []) | ||||||
|                 const list = featuresPerLayer.get(id) |  | ||||||
|                 if (list !== undefined) { |  | ||||||
|                     list.push(feature) |  | ||||||
|                 } else { |  | ||||||
|                     featuresPerLayer.set(id, [feature]) |  | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             for (const f of features) { |             for (const f of features) { | ||||||
|                 for (const layer of layers.data) { |                 for (const layer of layers.data) { | ||||||
|                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { |                     if (layer.layerDef.source.osmTags.matchesProperties(f.feature.properties)) { | ||||||
|                         // We have found our matching layer!
 |                         // We have found our matching layer!
 | ||||||
|                         addTo(layer, f) |                         featuresPerLayer.set(layer.layerDef.id, [f]) | ||||||
|                         if (!layer.layerDef.passAllFeatures) { |                         if (!layer.layerDef.passAllFeatures) { | ||||||
|                             // If not 'passAllFeatures', we are done for this feature
 |                             // If not 'passAllFeatures', we are done for this feature
 | ||||||
|                             break; |                             break; | ||||||
|  |  | ||||||
|  | @ -126,7 +126,7 @@ export default class GeoJsonSource implements FeatureSourceForLayer, Tiled { | ||||||
| 
 | 
 | ||||||
|                 eventSource.setData(eventSource.data.concat(newFeatures)) |                 eventSource.setData(eventSource.data.concat(newFeatures)) | ||||||
| 
 | 
 | ||||||
|             }).catch(msg => console.error("Could not load geojson layer", url, "due to", msg)) |             }).catch(msg => console.debug("Could not load geojson layer", url, "due to", msg)) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -5,13 +5,14 @@ import Loc from "../../../Models/Loc"; | ||||||
| import DynamicTileSource from "./DynamicTileSource"; | import DynamicTileSource from "./DynamicTileSource"; | ||||||
| import {Utils} from "../../../Utils"; | import {Utils} from "../../../Utils"; | ||||||
| import GeoJsonSource from "../Sources/GeoJsonSource"; | import GeoJsonSource from "../Sources/GeoJsonSource"; | ||||||
|  | import {BBox} from "../../BBox"; | ||||||
| 
 | 
 | ||||||
| export default class DynamicGeoJsonTileSource extends DynamicTileSource { | export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|     constructor(layer: FilteredLayer, |     constructor(layer: FilteredLayer, | ||||||
|                 registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, |                 registerLayer: (layer: FeatureSourceForLayer & Tiled) => void, | ||||||
|                 state: { |                 state: { | ||||||
|                     locationControl: UIEventSource<Loc> |                     locationControl: UIEventSource<Loc> | ||||||
|                     leafletMap: any |                     currentBounds: UIEventSource<BBox> | ||||||
|                 }) { |                 }) { | ||||||
|         const source = layer.layerDef.source |         const source = layer.layerDef.source | ||||||
|         if (source.geojsonZoomLevel === undefined) { |         if (source.geojsonZoomLevel === undefined) { | ||||||
|  | @ -29,7 +30,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource { | ||||||
|                 .replace("{x}_{y}.geojson", "overview.json") |                 .replace("{x}_{y}.geojson", "overview.json") | ||||||
|                 .replace("{layer}", layer.layerDef.id) |                 .replace("{layer}", layer.layerDef.id) | ||||||
| 
 | 
 | ||||||
|             Utils.downloadJson(whitelistUrl).then( |             Utils.downloadJsonCached(whitelistUrl, 1000*60*60).then( | ||||||
|                 json => { |                 json => { | ||||||
|                     const data = new Map<number, Set<number>>(); |                     const data = new Map<number, Set<number>>(); | ||||||
|                     for (const x in json) { |                     for (const x in json) { | ||||||
|  |  | ||||||
|  | @ -4,6 +4,7 @@ import {UIEventSource} from "../../UIEventSource"; | ||||||
| import Loc from "../../../Models/Loc"; | import Loc from "../../../Models/Loc"; | ||||||
| import TileHierarchy from "./TileHierarchy"; | import TileHierarchy from "./TileHierarchy"; | ||||||
| import {Tiles} from "../../../Models/TileRange"; | import {Tiles} from "../../../Models/TileRange"; | ||||||
|  | import {BBox} from "../../BBox"; | ||||||
| 
 | 
 | ||||||
| /*** | /*** | ||||||
|  * A tiled source which dynamically loads the required tiles at a fixed zoom level |  * A tiled source which dynamically loads the required tiles at a fixed zoom level | ||||||
|  | @ -17,8 +18,8 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|         zoomlevel: number, |         zoomlevel: number, | ||||||
|         constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), |         constructTile: (zxy: [number, number, number]) => (FeatureSourceForLayer & Tiled), | ||||||
|         state: { |         state: { | ||||||
|  |             currentBounds: UIEventSource<BBox>; | ||||||
|             locationControl: UIEventSource<Loc> |             locationControl: UIEventSource<Loc> | ||||||
|             leafletMap: any |  | ||||||
|         } |         } | ||||||
|     ) { |     ) { | ||||||
|         const self = this; |         const self = this; | ||||||
|  | @ -37,7 +38,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Yup, this is cheating to just get the bounds here
 |                 // Yup, this is cheating to just get the bounds here
 | ||||||
|                 const bounds = state.leafletMap.data?.getBounds() |                 const bounds = state.currentBounds.data | ||||||
|                 if (bounds === undefined) { |                 if (bounds === undefined) { | ||||||
|                     // We'll retry later
 |                     // We'll retry later
 | ||||||
|                     return undefined |                     return undefined | ||||||
|  | @ -50,7 +51,7 @@ export default class DynamicTileSource implements TileHierarchy<FeatureSourceFor | ||||||
|                 } |                 } | ||||||
|                 return needed |                 return needed | ||||||
|             } |             } | ||||||
|             , [layer.isDisplayed, state.leafletMap]).stabilized(250); |             , [layer.isDisplayed, state.currentBounds]).stabilized(250); | ||||||
| 
 | 
 | ||||||
|         neededTiles.addCallbackAndRunD(neededIndexes => { |         neededTiles.addCallbackAndRunD(neededIndexes => { | ||||||
|             console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) |             console.log("Tiled geojson source ", layer.layerDef.id, " needs", neededIndexes) | ||||||
|  |  | ||||||
|  | @ -63,10 +63,10 @@ export default class OsmFeatureSource { | ||||||
|             try { |             try { | ||||||
| 
 | 
 | ||||||
|                 for (const neededTile of neededTiles) { |                 for (const neededTile of neededTiles) { | ||||||
|                     console.log("Tile download", Tiles.tile_from_index(neededTile).join("/"), "started") |                     console.log("Tile download from OSM", Tiles.tile_from_index(neededTile).join("/"), "started") | ||||||
|                     self.downloadedTiles.add(neededTile) |                     self.downloadedTiles.add(neededTile) | ||||||
|                     self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => { |                     self.LoadTile(...Tiles.tile_from_index(neededTile)).then(_ => { | ||||||
|                         console.debug("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded") |                         console.debug("Tile ", Tiles.tile_from_index(neededTile).join("/"), "loaded from OSM") | ||||||
|                     }) |                     }) | ||||||
|                 } |                 } | ||||||
|             } catch (e) { |             } catch (e) { | ||||||
|  |  | ||||||
|  | @ -1,5 +1,4 @@ | ||||||
| import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | import {OsmNode, OsmObject, OsmRelation, OsmWay} from "./OsmObject"; | ||||||
| import State from "../../State"; |  | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import OsmChangeAction from "./Actions/OsmChangeAction"; | import OsmChangeAction from "./Actions/OsmChangeAction"; | ||||||
|  | @ -13,6 +12,7 @@ import {ElementStorage} from "../ElementStorage"; | ||||||
| import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler"; | import {GeoLocationPointProperties} from "../Actors/GeoLocationHandler"; | ||||||
| import {GeoOperations} from "../GeoOperations"; | import {GeoOperations} from "../GeoOperations"; | ||||||
| import {ChangesetTag} from "./ChangesetHandler"; | import {ChangesetTag} from "./ChangesetHandler"; | ||||||
|  | import {OsmConnection} from "./OsmConnection"; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Handles all changes made to OSM. |  * Handles all changes made to OSM. | ||||||
|  | @ -33,14 +33,23 @@ export class Changes { | ||||||
|     private readonly previouslyCreated: OsmObject[] = [] |     private readonly previouslyCreated: OsmObject[] = [] | ||||||
|     private readonly _leftRightSensitive: boolean; |     private readonly _leftRightSensitive: boolean; | ||||||
| 
 | 
 | ||||||
|     private _state: { allElements: ElementStorage; historicalUserLocations: FeatureSource } |     public readonly state: { allElements: ElementStorage; historicalUserLocations: FeatureSource; osmConnection: OsmConnection } | ||||||
|      |      | ||||||
|     constructor(leftRightSensitive: boolean = false) { |     public readonly extraComment:UIEventSource<string> = new UIEventSource(undefined) | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         state?: { | ||||||
|  |             allElements: ElementStorage, | ||||||
|  |             historicalUserLocations: FeatureSource, | ||||||
|  |             osmConnection: OsmConnection | ||||||
|  |         }, | ||||||
|  |         leftRightSensitive: boolean = false) { | ||||||
|         this._leftRightSensitive = leftRightSensitive; |         this._leftRightSensitive = leftRightSensitive; | ||||||
|         // We keep track of all changes just as well
 |         // We keep track of all changes just as well
 | ||||||
|         this.allChanges.setData([...this.pendingChanges.data]) |         this.allChanges.setData([...this.pendingChanges.data]) | ||||||
|         // If a pending change contains a negative ID, we save that
 |         // If a pending change contains a negative ID, we save that
 | ||||||
|         this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? []) |         this._nextId = Math.min(-1, ...this.pendingChanges.data?.map(pch => pch.id) ?? []) | ||||||
|  |         this.state = state; | ||||||
| 
 | 
 | ||||||
|         // Note: a changeset might be reused which was opened just before and might have already used some ids
 |         // Note: a changeset might be reused which was opened just before and might have already used some ids
 | ||||||
|         // This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
 |         // This doesn't matter however, as the '-1' is per piecewise upload, not global per changeset
 | ||||||
|  | @ -120,7 +129,7 @@ export class Changes { | ||||||
| 
 | 
 | ||||||
|     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { |     private calculateDistanceToChanges(change: OsmChangeAction, changeDescriptions: ChangeDescription[]) { | ||||||
| 
 | 
 | ||||||
|         if (this._state === undefined) { |         if (this.state === undefined) { | ||||||
|             // No state loaded -> we can't calculate...
 |             // No state loaded -> we can't calculate...
 | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|  | @ -129,7 +138,7 @@ export class Changes { | ||||||
|             return; |             return; | ||||||
|         } |         } | ||||||
|         const now = new Date() |         const now = new Date() | ||||||
|         const recentLocationPoints = this._state.historicalUserLocations.features.data.map(ff => ff.feature) |         const recentLocationPoints = this.state.historicalUserLocations.features.data.map(ff => ff.feature) | ||||||
|             .filter(feat => feat.geometry.type === "Point") |             .filter(feat => feat.geometry.type === "Point") | ||||||
|             .filter(feat => { |             .filter(feat => { | ||||||
|                 const visitTime = new Date((<GeoLocationPointProperties>feat.properties).date) |                 const visitTime = new Date((<GeoLocationPointProperties>feat.properties).date) | ||||||
|  | @ -149,7 +158,7 @@ export class Changes { | ||||||
| 
 | 
 | ||||||
|         const changedObjectCoordinates: [number, number][] = [] |         const changedObjectCoordinates: [number, number][] = [] | ||||||
| 
 | 
 | ||||||
|         const feature = this._state.allElements.ContainingFeatures.get(change.mainObjectId) |         const feature = this.state.allElements.ContainingFeatures.get(change.mainObjectId) | ||||||
|         if (feature !== undefined) { |         if (feature !== undefined) { | ||||||
|             changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature)) |             changedObjectCoordinates.push(GeoOperations.centerpointCoordinates(feature)) | ||||||
|         } |         } | ||||||
|  | @ -189,12 +198,6 @@ export class Changes { | ||||||
|         this.allChanges.ping() |         this.allChanges.ping() | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     public useLocationHistory(state: { |  | ||||||
|         allElements: ElementStorage, |  | ||||||
|         historicalUserLocations: FeatureSource |  | ||||||
|     }) { |  | ||||||
|         this._state = state |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     public registerIdRewrites(mappings: Map<string, string>): void { |     public registerIdRewrites(mappings: Map<string, string>): void { | ||||||
|         CreateNewNodeAction.registerIdRewrites(mappings) |         CreateNewNodeAction.registerIdRewrites(mappings) | ||||||
|  | @ -281,9 +284,14 @@ export class Changes { | ||||||
| 
 | 
 | ||||||
|         // This method is only called with changedescriptions for this theme
 |         // This method is only called with changedescriptions for this theme
 | ||||||
|         const theme = pending[0].meta.theme |         const theme = pending[0].meta.theme | ||||||
|  |         let comment = "Adding data with #MapComplete for theme #" + theme | ||||||
|  |         if(this.extraComment.data !== undefined){ | ||||||
|  |             comment+="\n\n"+this.extraComment.data | ||||||
|  |         } | ||||||
|  |          | ||||||
|         const metatags: ChangesetTag[] = [{ |         const metatags: ChangesetTag[] = [{ | ||||||
|             key: "comment", |             key: "comment", | ||||||
|             value: "Adding data with #MapComplete for theme #" + theme |             value: comment | ||||||
|         }, |         }, | ||||||
|             { |             { | ||||||
|                 key: "theme", |                 key: "theme", | ||||||
|  | @ -294,7 +302,7 @@ export class Changes { | ||||||
|             ...perBinMessage |             ...perBinMessage | ||||||
|         ] |         ] | ||||||
| 
 | 
 | ||||||
|         await State.state.osmConnection.changesetHandler.UploadChangeset( |         await this.state.osmConnection.changesetHandler.UploadChangeset( | ||||||
|             (csId) => Changes.createChangesetFor("" + csId, changes), |             (csId) => Changes.createChangesetFor("" + csId, changes), | ||||||
|             metatags |             metatags | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  | @ -1,9 +1,7 @@ | ||||||
| import escapeHtml from "escape-html"; | import escapeHtml from "escape-html"; | ||||||
| // @ts-ignore
 | import UserDetails, {OsmConnection} from "./OsmConnection"; | ||||||
| import {OsmConnection, UserDetails} from "./OsmConnection"; |  | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import {ElementStorage} from "../ElementStorage"; | import {ElementStorage} from "../ElementStorage"; | ||||||
| import State from "../../State"; |  | ||||||
| import Locale from "../../UI/i18n/Locale"; | import Locale from "../../UI/i18n/Locale"; | ||||||
| import Constants from "../../Models/Constants"; | import Constants from "../../Models/Constants"; | ||||||
| import {Changes} from "./Changes"; | import {Changes} from "./Changes"; | ||||||
|  | @ -287,8 +285,8 @@ export class ChangesetHandler { | ||||||
|                 ["language", Locale.language.data], |                 ["language", Locale.language.data], | ||||||
|                 ["host", window.location.host], |                 ["host", window.location.host], | ||||||
|                 ["path", path], |                 ["path", path], | ||||||
|                 ["source", State.state.currentUserLocation.features.data.length > 0 ? "survey" : undefined], |                 ["source", self.changes.state["currentUserLocation"]?.features?.data?.length > 0 ? "survey" : undefined], | ||||||
|                 ["imagery", State.state.backgroundLayer.data.id], |                 ["imagery", self.changes.state["backgroundLayer"]?.data?.id], | ||||||
|                 ...changesetTags.map(cstag => [cstag.key, cstag.value]) |                 ...changesetTags.map(cstag => [cstag.key, cstag.value]) | ||||||
|             ] |             ] | ||||||
|                 .filter(kv => (kv[1] ?? "") !== "") |                 .filter(kv => (kv[1] ?? "") !== "") | ||||||
|  |  | ||||||
|  | @ -70,7 +70,8 @@ export class OsmConnection { | ||||||
|                     // Used to keep multiple changesets open and to write to the correct changeset
 |                     // Used to keep multiple changesets open and to write to the correct changeset
 | ||||||
|                     layoutName: string, |                     layoutName: string, | ||||||
|                     singlePage?: boolean, |                     singlePage?: boolean, | ||||||
|                     osmConfiguration?: "osm" | "osm-test" |                     osmConfiguration?: "osm" | "osm-test", | ||||||
|  |                     attemptLogin?: true | boolean | ||||||
|                 } |                 } | ||||||
|     ) { |     ) { | ||||||
|         this.fakeUser = options.fakeUser ?? false; |         this.fakeUser = options.fakeUser ?? false; | ||||||
|  | @ -117,7 +118,7 @@ export class OsmConnection { | ||||||
|             options.oauth_token.setData(undefined); |             options.oauth_token.setData(undefined); | ||||||
| 
 | 
 | ||||||
|         } |         } | ||||||
|         if (this.auth.authenticated()) { |         if (this.auth.authenticated() && (options.attemptLogin !== false)) { | ||||||
|             this.AttemptLogin(); // Also updates the user badge
 |             this.AttemptLogin(); // Also updates the user badge
 | ||||||
|         } else { |         } else { | ||||||
|             console.log("Not authenticated"); |             console.log("Not authenticated"); | ||||||
|  |  | ||||||
|  | @ -49,7 +49,8 @@ export default class ElementsState extends FeatureSwitchState { | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig) { | ||||||
|         super(layoutToUse); |         super(layoutToUse); | ||||||
| 
 | 
 | ||||||
|         this.changes = new Changes(layoutToUse?.isLeftRightSensitive() ?? false) |         // @ts-ignore
 | ||||||
|  |         this.changes = new Changes(this,layoutToUse?.isLeftRightSensitive() ?? false) | ||||||
|         { |         { | ||||||
|             // -- Location control initialization
 |             // -- Location control initialization
 | ||||||
|             const zoom = UIEventSource.asFloat( |             const zoom = UIEventSource.asFloat( | ||||||
|  |  | ||||||
|  | @ -9,6 +9,7 @@ import MapState from "./MapState"; | ||||||
| import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; | ||||||
| import Hash from "../Web/Hash"; | import Hash from "../Web/Hash"; | ||||||
| import {BBox} from "../BBox"; | import {BBox} from "../BBox"; | ||||||
|  | import {FeatureSourceForLayer} from "../FeatureSource/FeatureSource"; | ||||||
| 
 | 
 | ||||||
| export default class FeaturePipelineState extends MapState { | export default class FeaturePipelineState extends MapState { | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -82,8 +82,8 @@ export default class MapState extends UserRelatedState { | ||||||
|     public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[] |     public overlayToggles: { config: TilesourceConfig, isDisplayed: UIEventSource<boolean> }[] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig, options?: {attemptLogin: true | boolean}) { | ||||||
|         super(layoutToUse); |         super(layoutToUse, options); | ||||||
| 
 | 
 | ||||||
|         this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl); |         this.availableBackgroundLayers = AvailableBaseLayers.AvailableLayersAt(this.locationControl); | ||||||
| 
 | 
 | ||||||
|  | @ -265,7 +265,6 @@ export default class MapState extends UserRelatedState { | ||||||
| 
 | 
 | ||||||
|         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0] |         let gpsLayerDef: FilteredLayer = this.filteredLayers.data.filter(l => l.layerDef.id === "gps_location_history")[0] | ||||||
|         this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features); |         this.historicalUserLocations = new SimpleFeatureSource(gpsLayerDef, Tiles.tile_index(0, 0, 0), features); | ||||||
|         this.changes.useLocationHistory(this) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const asLine = features.map(allPoints => { |         const asLine = features.map(allPoints => { | ||||||
|  |  | ||||||
|  | @ -36,7 +36,7 @@ export default class UserRelatedState extends ElementsState { | ||||||
|     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; |     public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|     constructor(layoutToUse: LayoutConfig) { |     constructor(layoutToUse: LayoutConfig, options:{attemptLogin : true | boolean}) { | ||||||
|         super(layoutToUse); |         super(layoutToUse); | ||||||
| 
 | 
 | ||||||
|         this.osmConnection = new OsmConnection({ |         this.osmConnection = new OsmConnection({ | ||||||
|  | @ -50,7 +50,8 @@ export default class UserRelatedState extends ElementsState { | ||||||
|                 "Used to complete the login" |                 "Used to complete the login" | ||||||
|             ), |             ), | ||||||
|             layoutName: layoutToUse?.id, |             layoutName: layoutToUse?.id, | ||||||
|             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data |             osmConfiguration: <'osm' | 'osm-test'>this.featureSwitchApiURL.data, | ||||||
|  |             attemptLogin: options?.attemptLogin | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|         this.mangroveIdentity = new MangroveIdentity( |         this.mangroveIdentity = new MangroveIdentity( | ||||||
|  |  | ||||||
|  | @ -289,6 +289,7 @@ export class UIEventSource<T> { | ||||||
| 
 | 
 | ||||||
|         const stack = new Error().stack.split("\n"); |         const stack = new Error().stack.split("\n"); | ||||||
|         const callee = stack[1] |         const callee = stack[1] | ||||||
|  |          | ||||||
|         const newSource = new UIEventSource<J>( |         const newSource = new UIEventSource<J>( | ||||||
|             f(this.data), |             f(this.data), | ||||||
|             "map(" + this.tag + ")@"+callee |             "map(" + this.tag + ")@"+callee | ||||||
|  | @ -298,7 +299,7 @@ export class UIEventSource<T> { | ||||||
|             newSource.setData(f(self.data)); |             newSource.setData(f(self.data)); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.addCallbackAndRun(update); |         this.addCallback(update); | ||||||
|         for (const extraSource of extraSources) { |         for (const extraSource of extraSources) { | ||||||
|             extraSource?.addCallback(update); |             extraSource?.addCallback(update); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -2,7 +2,7 @@ import {Utils} from "../Utils"; | ||||||
| 
 | 
 | ||||||
| export default class Constants { | export default class Constants { | ||||||
| 
 | 
 | ||||||
|     public static vNumber = "0.13.0-alpha-6"; |     public static vNumber = "0.13.0-alpha-7"; | ||||||
|     public static ImgurApiKey = '7070e7167f0a25a' |     public static ImgurApiKey = '7070e7167f0a25a' | ||||||
|     public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" |     public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -5,7 +5,6 @@ import Title from "./Base/Title"; | ||||||
| import Toggle from "./Input/Toggle"; | import Toggle from "./Input/Toggle"; | ||||||
| import {SubtleButton} from "./Base/SubtleButton"; | import {SubtleButton} from "./Base/SubtleButton"; | ||||||
| import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | import LayoutConfig from "../Models/ThemeConfig/LayoutConfig"; | ||||||
| import UserRelatedState from "../Logic/State/UserRelatedState"; |  | ||||||
| import ValidatedTextField from "./Input/ValidatedTextField"; | import ValidatedTextField from "./Input/ValidatedTextField"; | ||||||
| import {Utils} from "../Utils"; | import {Utils} from "../Utils"; | ||||||
| import {UIEventSource} from "../Logic/UIEventSource"; | import {UIEventSource} from "../Logic/UIEventSource"; | ||||||
|  | @ -16,11 +15,19 @@ import {LocalStorageSource} from "../Logic/Web/LocalStorageSource"; | ||||||
| import {DropDown} from "./Input/DropDown"; | import {DropDown} from "./Input/DropDown"; | ||||||
| import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||||
| import MinimapImplementation from "./Base/MinimapImplementation"; | import MinimapImplementation from "./Base/MinimapImplementation"; | ||||||
| import State from "../State"; |  | ||||||
| import {OsmConnection} from "../Logic/Osm/OsmConnection"; | import {OsmConnection} from "../Logic/Osm/OsmConnection"; | ||||||
| import FeaturePipelineState from "../Logic/State/FeaturePipelineState"; | import {BBox} from "../Logic/BBox"; | ||||||
|  | import MapState from "../Logic/State/MapState"; | ||||||
|  | import FeaturePipeline from "../Logic/FeatureSource/FeaturePipeline"; | ||||||
|  | import LayerConfig from "../Models/ThemeConfig/LayerConfig"; | ||||||
|  | import TagRenderingConfig from "../Models/ThemeConfig/TagRenderingConfig"; | ||||||
|  | import FeatureSource from "../Logic/FeatureSource/FeatureSource"; | ||||||
|  | import List from "./Base/List"; | ||||||
|  | import {QueryParameters} from "../Logic/Web/QueryParameters"; | ||||||
|  | import {SubstitutedTranslation} from "./SubstitutedTranslation"; | ||||||
|  | import {AutoAction} from "./Popup/AutoApplyButton"; | ||||||
| 
 | 
 | ||||||
| export default class AutomatonGui extends Combine { | class AutomatonGui extends Combine { | ||||||
| 
 | 
 | ||||||
|     constructor() { |     constructor() { | ||||||
| 
 | 
 | ||||||
|  | @ -28,7 +35,8 @@ export default class AutomatonGui extends Combine { | ||||||
|             allElements: undefined, |             allElements: undefined, | ||||||
|             changes: undefined, |             changes: undefined, | ||||||
|             layoutName: "automaton", |             layoutName: "automaton", | ||||||
|             singlePage: true |             singlePage: false, | ||||||
|  |             oauth_token: QueryParameters.GetQueryParameter("oauth_token", "OAuth token") | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         super([ |         super([ | ||||||
|  | @ -39,35 +47,174 @@ export default class AutomatonGui extends Combine { | ||||||
|             ]).SetClass("flex"), |             ]).SetClass("flex"), | ||||||
|             new Toggle( |             new Toggle( | ||||||
|                 AutomatonGui.GenerateMainPanel(), |                 AutomatonGui.GenerateMainPanel(), | ||||||
|                 new SubtleButton(Svg.osm_logo_svg(), "Login to get started"), |                 new SubtleButton(Svg.osm_logo_svg(), "Login to get started").onClick(() => osmConnection.AttemptLogin()), | ||||||
|                 osmConnection.isLoggedIn |                 osmConnection.isLoggedIn | ||||||
|             )]) |             )]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static AutomationPanel(layoutToUse: LayoutConfig, tiles: UIEventSource<number[]>): BaseUIElement { |     private static startedTiles = new Set<number>() | ||||||
|         const handledTiles = new UIEventSource(0) |  | ||||||
| 
 | 
 | ||||||
|         const state = new FeaturePipelineState(layoutToUse) |     private static TileHandler(layoutToUse: LayoutConfig, tileIndex: number, targetLayer: string, targetAction: TagRenderingConfig, extraCommentText: UIEventSource<string>, whenDone: ((result: string) => void)): BaseUIElement { | ||||||
| 
 | 
 | ||||||
|  |         if (AutomatonGui.startedTiles.has(tileIndex)) { | ||||||
|  |             throw "Already started tile " + tileIndex | ||||||
|  |         } | ||||||
|  |         AutomatonGui.startedTiles.add(tileIndex) | ||||||
| 
 | 
 | ||||||
|         const nextTile = tiles.map(indices => { |         const state = new MapState(layoutToUse, {attemptLogin: false}) | ||||||
|             if (indices === undefined) { |         extraCommentText.syncWith( state.changes.extraComment) | ||||||
|                 return "No tiles loaded - can not automate"; |         const [z, x, y] = Tiles.tile_from_index(tileIndex) | ||||||
|             } |         state.locationControl.setData({ | ||||||
|             const currentTile = handledTiles.data |             zoom: z, | ||||||
|             const tileIndex = indices[currentTile] |             lon: x, | ||||||
|             if (tileIndex === undefined) { |             lat: y | ||||||
|                 return "All done!"; |         }) | ||||||
|  |         state.currentBounds.setData( | ||||||
|  |             BBox.fromTileIndex(tileIndex) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         let targetTiles: UIEventSource<FeatureSource[]> = new UIEventSource<FeatureSource[]>([]) | ||||||
|  |         const pipeline = new FeaturePipeline((tile => { | ||||||
|  |             const layerId = tile.layer.layerDef.id | ||||||
|  |             if (layerId === targetLayer) { | ||||||
|  |                 targetTiles.data.push(tile) | ||||||
|  |                 targetTiles.ping() | ||||||
|             } |             } | ||||||
|  |         }), state) | ||||||
| 
 | 
 | ||||||
|  |         state.locationControl.ping(); | ||||||
|  |         state.currentBounds.ping(); | ||||||
|  |         const stateToShow = new UIEventSource("") | ||||||
| 
 | 
 | ||||||
|             return "" + tileIndex |         pipeline.runningQuery.map( | ||||||
|         }, [handledTiles]) |             async isRunning => { | ||||||
|  |                 if (targetTiles.data.length === 0) { | ||||||
|  |                     stateToShow.setData("No data loaded yet...") | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if (isRunning) { | ||||||
|  |                     stateToShow.setData("Waiting for all layers to be loaded... Has " + targetTiles.data.length + " tiles already") | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 if (targetTiles.data.length === 0) { | ||||||
|  |                     stateToShow.setData("No features found to apply the action") | ||||||
|  |                     whenDone("empty") | ||||||
|  |                     return true; | ||||||
|  |                 } | ||||||
|  |                 stateToShow.setData("Applying metatags") | ||||||
|  |                 pipeline.updateAllMetaTagging() | ||||||
|  |                 stateToShow.setData("Gathering applicable elements") | ||||||
|  | 
 | ||||||
|  |                 let handled = 0 | ||||||
|  |                 let inspected = 0 | ||||||
|  |                 for (const targetTile of targetTiles.data) { | ||||||
|  | 
 | ||||||
|  |                     for (const ffs of targetTile.features.data) { | ||||||
|  |                         inspected++ | ||||||
|  |                         if (inspected % 10 === 0) { | ||||||
|  |                             stateToShow.setData("Inspected " + inspected + " features, updated " + handled + " features") | ||||||
|  |                         } | ||||||
|  |                         const feature = ffs.feature | ||||||
|  |                         const rendering = targetAction.GetRenderValue(feature.properties).txt | ||||||
|  |                         const actions = Utils.NoNull(SubstitutedTranslation.ExtractSpecialComponents(rendering) | ||||||
|  |                             .map(obj => obj.special)) | ||||||
|  |                         for (const action of actions) { | ||||||
|  |                             const auto = <AutoAction>action.func | ||||||
|  |                             if (auto.supportsAutoAction !== true) { | ||||||
|  |                                 continue | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             await auto.applyActionOn({ | ||||||
|  |                                 layoutToUse: state.layoutToUse, | ||||||
|  |                                 changes: state.changes | ||||||
|  |                             }, state.allElements.getEventSourceById(feature.properties.id), action.args) | ||||||
|  |                             handled++ | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 stateToShow.setData("Done! Inspected " + inspected + " features, updated " + handled + " features") | ||||||
|  | 
 | ||||||
|  |                 if (inspected === 0) { | ||||||
|  |                     whenDone("empty") | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (handled === 0) { | ||||||
|  |                     window.setTimeout(() => whenDone("no-action"), 1000) | ||||||
|  |                 }else{ | ||||||
|  |                     state.changes.flushChanges("handled tile automatically, time to flush!") | ||||||
|  |                     whenDone("fixed") | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |             }, [targetTiles]) | ||||||
| 
 | 
 | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             new VariableUiElement(handledTiles.map(i => "" + i)), |             new Title("Performing action for tile " + tileIndex, 1), | ||||||
|             new VariableUiElement(nextTile) |             new VariableUiElement(stateToShow)]).SetClass("flex flex-col") | ||||||
|         ]) |     } | ||||||
|  | 
 | ||||||
|  |     private static AutomationPanel(layoutToUse: LayoutConfig, indices: number[], extraCommentText: UIEventSource<string>, tagRenderingToAutomate: { layer: LayerConfig, tagRendering: TagRenderingConfig }): BaseUIElement { | ||||||
|  |         const layerId = tagRenderingToAutomate.layer.id | ||||||
|  |         const trId = tagRenderingToAutomate.tagRendering.id | ||||||
|  |         const tileState = LocalStorageSource.GetParsed("automation-tile_state-" + layerId + "-" + trId, {}) | ||||||
|  | 
 | ||||||
|  |         if (indices === undefined) { | ||||||
|  |             return new FixedUiElement("No tiles loaded - can not automate") | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const nextTileToHandle = tileState.map(handledTiles => { | ||||||
|  |             for (const index of indices) { | ||||||
|  |                 if (handledTiles[index] !== undefined) { | ||||||
|  |                     // Already handled
 | ||||||
|  |                     continue | ||||||
|  |                 } | ||||||
|  |                 return index | ||||||
|  |             } | ||||||
|  |             return undefined | ||||||
|  |         }) | ||||||
|  |         nextTileToHandle.addCallback(t => console.warn("Next tile to handle is", t)) | ||||||
|  | 
 | ||||||
|  |         const neededTimes = new UIEventSource<number[]>([]) | ||||||
|  |         const automaton = new VariableUiElement(nextTileToHandle.map(tileIndex => { | ||||||
|  |             if (tileIndex === undefined) { | ||||||
|  |                 return new FixedUiElement("All done!").SetClass("thanks") | ||||||
|  |             } | ||||||
|  |             console.warn("Triggered map on nextTileToHandle",tileIndex) | ||||||
|  |             const start = new Date() | ||||||
|  |             return AutomatonGui.TileHandler(layoutToUse, tileIndex, layerId, tagRenderingToAutomate.tagRendering, extraCommentText,(result) => { | ||||||
|  |                 const end = new Date() | ||||||
|  |                 const timeNeeded = (end.getTime() - start.getTime()) / 1000; | ||||||
|  |                 neededTimes.data.push(timeNeeded) | ||||||
|  |                 neededTimes.ping() | ||||||
|  |                 tileState.data[tileIndex] = result | ||||||
|  |                 tileState.ping(); | ||||||
|  |             }); | ||||||
|  |         })) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         const statistics = new VariableUiElement(tileState.map(states => { | ||||||
|  |             let total = 0 | ||||||
|  |             const perResult = new Map<string, number>() | ||||||
|  |             for (const key in states) { | ||||||
|  |                 total++ | ||||||
|  |                 const result = states[key] | ||||||
|  |                 perResult.set(result, (perResult.get(result) ?? 0) + 1) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let sum = 0 | ||||||
|  |             neededTimes.data.forEach(v => { | ||||||
|  |                 sum = sum + v | ||||||
|  |             }) | ||||||
|  |             let timePerTile = sum / neededTimes.data.length | ||||||
|  | 
 | ||||||
|  |             return new Combine(["Handled " + total + "/" + indices.length + " tiles: ", | ||||||
|  |                 new List(Array.from(perResult.keys()).map(key => key + ": " + perResult.get(key))), | ||||||
|  |                 "Handling one tile needs " + (Math.floor(timePerTile * 100) / 100) + "s on average. Estimated time left: " + Math.floor((indices.length - total) * timePerTile) + "s" | ||||||
|  |             ]).SetClass("flex flex-col") | ||||||
|  |         })) | ||||||
|  | 
 | ||||||
|  |         return new Combine([statistics, automaton]).SetClass("flex flex-col") | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GenerateMainPanel(): BaseUIElement { |     private static GenerateMainPanel(): BaseUIElement { | ||||||
|  | @ -85,18 +232,22 @@ export default class AutomatonGui extends Combine { | ||||||
|         tilepath.SetClass("w-full") |         tilepath.SetClass("w-full") | ||||||
|         LocalStorageSource.Get("automation-tile_path").syncWith(tilepath.GetValue(), true) |         LocalStorageSource.Get("automation-tile_path").syncWith(tilepath.GetValue(), true) | ||||||
| 
 | 
 | ||||||
|         const tilesToRunOver = tilepath.GetValue().bind(path => { |          | ||||||
|  |         let tilesToRunOver = tilepath.GetValue().bind(path => { | ||||||
|             if (path === undefined) { |             if (path === undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             return UIEventSource.FromPromiseWithErr(Utils.downloadJson(path)) |             return UIEventSource.FromPromiseWithErr(Utils.downloadJsonCached(path,1000*60*60)) | ||||||
|         }) |         }) | ||||||
|          |          | ||||||
|  |         const targetZoom = 14 | ||||||
|  | 
 | ||||||
|         const tilesPerIndex = tilesToRunOver.map(tiles => { |         const tilesPerIndex = tilesToRunOver.map(tiles => { | ||||||
|  |              | ||||||
|             if (tiles === undefined || tiles["error"] !== undefined) { |             if (tiles === undefined || tiles["error"] !== undefined) { | ||||||
|                 return undefined |                 return undefined | ||||||
|             } |             } | ||||||
|             let indexes = []; |             let indexes : number[] = []; | ||||||
|             const tilesS = tiles["success"] |             const tilesS = tiles["success"] | ||||||
|             const z = Number(tilesS["zoom"]) |             const z = Number(tilesS["zoom"]) | ||||||
|             for (const key in tilesS) { |             for (const key in tilesS) { | ||||||
|  | @ -107,13 +258,31 @@ export default class AutomatonGui extends Combine { | ||||||
|                 const ys = tilesS[key] |                 const ys = tilesS[key] | ||||||
|                 indexes.push(...ys.map(y => Tiles.tile_index(z, x, y))) |                 indexes.push(...ys.map(y => Tiles.tile_index(z, x, y))) | ||||||
|             } |             } | ||||||
|             return indexes | 
 | ||||||
|  |             console.log("Got ", indexes.length, "indexes") | ||||||
|  |             let rezoomed = new Set<number>() | ||||||
|  |             for (const index of indexes) { | ||||||
|  |                 let [z, x, y] = Tiles.tile_from_index(index) | ||||||
|  |                 while (z > targetZoom) { | ||||||
|  |                     z-- | ||||||
|  |                     x = Math.floor(x / 2) | ||||||
|  |                     y = Math.floor(y / 2) | ||||||
|  |                 } | ||||||
|  |                 rezoomed.add(Tiles.tile_index(z, x, y)) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |             return Array.from(rezoomed) | ||||||
|         }) |         }) | ||||||
| 
 | 
 | ||||||
|  |         const extraComment = ValidatedTextField.InputForType("text") | ||||||
|  |         LocalStorageSource.Get("automaton-extra-comment").syncWith(extraComment.GetValue()) | ||||||
|  | 
 | ||||||
|         return new Combine([ |         return new Combine([ | ||||||
|             themeSelect, |             themeSelect, | ||||||
|             "Specify the path to a tile overview. This is a hosted .json of the format {x : [y0, y1, y2], x1: [y0, ...]} where x is a string and y are numbers", |             "Specify the path to a tile overview. This is a hosted .json of the format {x : [y0, y1, y2], x1: [y0, ...]} where x is a string and y are numbers", | ||||||
|             tilepath, |             tilepath, | ||||||
|  |             extraComment, | ||||||
|             new VariableUiElement(tilesToRunOver.map(t => { |             new VariableUiElement(tilesToRunOver.map(t => { | ||||||
|                 if (t === undefined) { |                 if (t === undefined) { | ||||||
|                     return "No path given or still loading..." |                     return "No path given or still loading..." | ||||||
|  | @ -128,10 +297,43 @@ export default class AutomatonGui extends Combine { | ||||||
|                 if (layoutToUse === undefined) { |                 if (layoutToUse === undefined) { | ||||||
|                     return new FixedUiElement("Select a valid layout") |                     return new FixedUiElement("Select a valid layout") | ||||||
|                 } |                 } | ||||||
|  |                 if (tilesPerIndex.data === undefined || tilesPerIndex.data.length === 0) { | ||||||
|  |                     return "No tiles given" | ||||||
|  |                 } | ||||||
| 
 | 
 | ||||||
|                 return AutomatonGui.AutomationPanel(layoutToUse, tilesPerIndex) |                 const automatableTagRenderings: { layer: LayerConfig, tagRendering: TagRenderingConfig }[] = [] | ||||||
|  |                 for (const layer of layoutToUse.layers) { | ||||||
|  |                     for (const tagRendering of layer.tagRenderings) { | ||||||
|  |                         if (tagRendering.group === "auto") { | ||||||
|  |                             automatableTagRenderings.push({layer, tagRendering: tagRendering}) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 console.log("Automatable tag renderings:", automatableTagRenderings) | ||||||
|  |                 if (automatableTagRenderings.length === 0) { | ||||||
|  |                     return new FixedUiElement('This theme does not have any tagRendering with "group": "auto" set').SetClass("alert") | ||||||
|  |                 } | ||||||
|  |                 const pickAuto = new DropDown("Pick the action to automate", | ||||||
|  |                     [ | ||||||
|  |                         { | ||||||
|  |                             value: undefined, | ||||||
|  |                             shown: "Pick an option" | ||||||
|  |                         }, | ||||||
|  |                         ...automatableTagRenderings.map(config => ( | ||||||
|  |                             { | ||||||
|  |                                 shown: config.layer.id + " - " + config.tagRendering.id, | ||||||
|  |                                 value: config | ||||||
|  |                             } | ||||||
|  |                         )) | ||||||
|  |                     ] | ||||||
|  |                 ) | ||||||
| 
 | 
 | ||||||
|             })) | 
 | ||||||
|  |                 return new Combine([ | ||||||
|  |                     pickAuto, | ||||||
|  |                     new VariableUiElement(pickAuto.GetValue().map(auto => auto === undefined ? undefined : AutomatonGui.AutomationPanel(layoutToUse, tilesPerIndex.data, extraComment.GetValue(), auto)))]) | ||||||
|  | 
 | ||||||
|  |             }, [tilesPerIndex])).SetClass("flex flex-col") | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         ]).SetClass("flex flex-col") |         ]).SetClass("flex flex-col") | ||||||
|  |  | ||||||
|  | @ -17,11 +17,16 @@ import {VariableUiElement} from "../Base/VariableUIElement"; | ||||||
| import Loading from "../Base/Loading"; | import Loading from "../Base/Loading"; | ||||||
| import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | import {OsmConnection} from "../../Logic/Osm/OsmConnection"; | ||||||
| import Translations from "../i18n/Translations"; | import Translations from "../i18n/Translations"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {Changes} from "../../Logic/Osm/Changes"; | ||||||
| 
 | 
 | ||||||
| export interface AutoAction extends SpecialVisualization { | export interface AutoAction extends SpecialVisualization { | ||||||
|     supportsAutoAction: boolean |     supportsAutoAction: boolean | ||||||
| 
 | 
 | ||||||
|     applyActionOn(state: FeaturePipelineState, tagSource: UIEventSource<any>, argument: string[]): Promise<void> |     applyActionOn(state: { | ||||||
|  |         layoutToUse: LayoutConfig, | ||||||
|  |         changes: Changes | ||||||
|  |     }, tagSource: UIEventSource<any>, argument: string[]): Promise<void> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export default class AutoApplyButton implements SpecialVisualization { | export default class AutoApplyButton implements SpecialVisualization { | ||||||
|  |  | ||||||
|  | @ -12,6 +12,8 @@ import Toggle from "../Input/Toggle"; | ||||||
| import {Utils} from "../../Utils"; | import {Utils} from "../../Utils"; | ||||||
| import {Tag} from "../../Logic/Tags/Tag"; | import {Tag} from "../../Logic/Tags/Tag"; | ||||||
| import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; | import FeaturePipelineState from "../../Logic/State/FeaturePipelineState"; | ||||||
|  | import LayoutConfig from "../../Models/ThemeConfig/LayoutConfig"; | ||||||
|  | import {Changes} from "../../Logic/Osm/Changes"; | ||||||
| 
 | 
 | ||||||
| export default class TagApplyButton implements AutoAction { | export default class TagApplyButton implements AutoAction { | ||||||
|     public readonly funcName = "tag_apply"; |     public readonly funcName = "tag_apply"; | ||||||
|  | @ -79,7 +81,10 @@ export default class TagApplyButton implements AutoAction { | ||||||
| 
 | 
 | ||||||
|     public readonly example = "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"; |     public readonly example = "`{tag_apply(survey_date=$_now:date, Surveyed today!)}`, `{tag_apply(addr:street=$addr:street, Apply the address, apply_icon.svg, _closest_osm_id)"; | ||||||
| 
 | 
 | ||||||
|     async applyActionOn(state: FeaturePipelineState, tags: UIEventSource<any>, args: string[]) : Promise<void>{ |     async applyActionOn(state: { | ||||||
|  |         layoutToUse: LayoutConfig, | ||||||
|  |         changes: Changes | ||||||
|  |     }, tags: UIEventSource<any>, args: string[]) : Promise<void>{ | ||||||
|         const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) |         const tagsToApply = TagApplyButton.generateTagsToApply(args[0], tags) | ||||||
|         const targetIdKey = args[3] |         const targetIdKey = args[3] | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -76,6 +76,10 @@ | ||||||
|     { |     { | ||||||
|       "builtin": "crab_address", |       "builtin": "crab_address", | ||||||
|       "override": { |       "override": { | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "http://127.0.0.1:8080/tile_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 18 | ||||||
|  |         }, | ||||||
|         "mapRendering": [ |         "mapRendering": [ | ||||||
|           { |           { | ||||||
|             "iconSize": "5,5,center", |             "iconSize": "5,5,center", | ||||||
|  | @ -101,8 +105,8 @@ | ||||||
|         "_embedded_crab_addresses:=Array.from(new Set(feat.overlapWith('crab_address').map(ff => ff.feat.properties).filter(p => p._HNRLABEL.toLowerCase() === (feat.properties['addr:housenumber'] + (feat.properties['addr:unit']??'')).toLowerCase()).map(p => p.STRAATNM)))", |         "_embedded_crab_addresses:=Array.from(new Set(feat.overlapWith('crab_address').map(ff => ff.feat.properties).filter(p => p._HNRLABEL.toLowerCase() === (feat.properties['addr:housenumber'] + (feat.properties['addr:unit']??'')).toLowerCase()).map(p => p.STRAATNM)))", | ||||||
|         "_singular_import:=feat.get('_embedded_crab_addresses')?.length == 1", |         "_singular_import:=feat.get('_embedded_crab_addresses')?.length == 1", | ||||||
|         "_name_to_apply:=feat.get('_embedded_crab_addresses')[0]", |         "_name_to_apply:=feat.get('_embedded_crab_addresses')[0]", | ||||||
|         "_nearby_street_names:=feat.closestn('named_streets',5,'name', 500).map(ff => ff.feat.properties.name)", |         "_nearby_street_names:=feat.closestn('named_streets',5,'name', 1000).map(ff => [ff.feat.properties.name, ff.feat.properties['alt_name'], ff.feat.properties['name:nl']])", | ||||||
|         "_spelling_is_correct:= feat.get('_nearby_street_names').indexOf(feat.properties['_name_to_apply']) >= 0" |         "_spelling_is_correct:= [].concat(...feat.get('_nearby_street_names')).indexOf(feat.properties['_name_to_apply']) >= 0" | ||||||
|       ], |       ], | ||||||
|       "mapRendering": [ |       "mapRendering": [ | ||||||
|         { |         { | ||||||
|  | @ -134,6 +138,7 @@ | ||||||
|       "tagRenderings": [ |       "tagRenderings": [ | ||||||
|         { |         { | ||||||
|           "id": "apply_streetname", |           "id": "apply_streetname", | ||||||
|  |           "group": "auto", | ||||||
|           "render": "{tag_apply(addr:street=$_name_to_apply ,Apply the CRAB-street onto this building)}", |           "render": "{tag_apply(addr:street=$_name_to_apply ,Apply the CRAB-street onto this building)}", | ||||||
|           "mappings": [ |           "mappings": [ | ||||||
|             { |             { | ||||||
|  | @ -146,8 +151,7 @@ | ||||||
|             } |             } | ||||||
|           ] |           ] | ||||||
|         } |         } | ||||||
|       ], |       ] | ||||||
|       "passAllFeatures": true |  | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "hideFromOverview": true |   "hideFromOverview": true | ||||||
|  |  | ||||||
|  | @ -2,7 +2,6 @@ | ||||||
|  * Generates an overview for which tiles exist and which don't |  * Generates an overview for which tiles exist and which don't | ||||||
|  */ |  */ | ||||||
| import ScriptUtils from "./ScriptUtils"; | import ScriptUtils from "./ScriptUtils"; | ||||||
| import {Tiles} from "../Models/TileRange"; |  | ||||||
| import {writeFileSync} from "fs"; | import {writeFileSync} from "fs"; | ||||||
| 
 | 
 | ||||||
| function main(args: string[]) { | function main(args: string[]) { | ||||||
|  | @ -28,10 +27,10 @@ function main(args: string[]) { | ||||||
|          |          | ||||||
|         const x = match[2] |         const x = match[2] | ||||||
|         const y = match[3] |         const y = match[3] | ||||||
|         if(!indices[x] !== undefined){ |         if(indices[x] === undefined){ | ||||||
|             indices[x] = [] |             indices[x] = [] | ||||||
|         } |         } | ||||||
|         indices[x] .push(Number(y)) |         indices[x].push(Number(y)) | ||||||
|     } |     } | ||||||
|     indices["zoom"] = zoomLevel; |     indices["zoom"] = zoomLevel; | ||||||
|     const match = files[0].match("\(.*\)_\([0-9]*\)_\([0-9]*\)_\([0-9]*\).geojson") |     const match = files[0].match("\(.*\)_\([0-9]*\)_\([0-9]*\)_\([0-9]*\).geojson") | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue