forked from MapComplete/MapComplete
		
	Add capability to load tiled geojsons, eventually as overpass-cache
This commit is contained in:
		
							parent
							
								
									475cdae19f
								
							
						
					
					
						commit
						2da52501a3
					
				
					 16 changed files with 520 additions and 76 deletions
				
			
		|  | @ -85,6 +85,7 @@ export default class LayerConfig { | ||||||
|             this.source = new SourceConfig({ |             this.source = new SourceConfig({ | ||||||
|                 osmTags: osmTags, |                 osmTags: osmTags, | ||||||
|                 geojsonSource: json.source["geoJson"], |                 geojsonSource: json.source["geoJson"], | ||||||
|  |                 geojsonSourceLevel: json.source["geoJsonZoomLevel"], | ||||||
|                 overpassScript: json.source["overpassScript"], |                 overpassScript: json.source["overpassScript"], | ||||||
|             }); |             }); | ||||||
|         } else { |         } else { | ||||||
|  | @ -159,7 +160,7 @@ export default class LayerConfig { | ||||||
| 
 | 
 | ||||||
|                         if (renderingJson === "questions") { |                         if (renderingJson === "questions") { | ||||||
|                             if (readOnly) { |                             if (readOnly) { | ||||||
|                                 throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}` |                                 throw `A tagrendering has a question, but asking a question does not make sense here: is it a title icon or a geojson-layer? ${context}. The offending tagrendering is ${JSON.stringify(renderingJson)}` | ||||||
|                             } |                             } | ||||||
| 
 | 
 | ||||||
|                             return new TagRenderingConfig("questions", undefined) |                             return new TagRenderingConfig("questions", undefined) | ||||||
|  | @ -176,7 +177,7 @@ export default class LayerConfig { | ||||||
|                 }); |                 }); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         this.tagRenderings = trs(json.tagRenderings, this.source.geojsonSource !== undefined); |         this.tagRenderings = trs(json.tagRenderings, false); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|         const titleIcons = []; |         const titleIcons = []; | ||||||
|  |  | ||||||
|  | @ -29,7 +29,8 @@ export interface LayerConfigJson { | ||||||
|      * There are some options: |      * There are some options: | ||||||
|      * |      * | ||||||
|      * source: {osmTags: "key=value"} will fetch all objects with given tags from OSM. Currently, this will create a query to overpass and fetch the data - in the future this might fetch from the OSM API |      * source: {osmTags: "key=value"} will fetch all objects with given tags from OSM. Currently, this will create a query to overpass and fetch the data - in the future this might fetch from the OSM API | ||||||
|      * source: {geoJsonSource: "https://my.source.net/some-geo-data.geojson"} to fetch a geojson from a third party source |      * source: {geoJson: "https://my.source.net/some-geo-data.geojson"} to fetch a geojson from a third party source | ||||||
|  |      * source: {geoJson: "https://my.source.net/some-tile-geojson-{z}-{x}-{y}.geojson", geoJsonZoomLevel: 14} to use a tiled geojson source. The web server must offer multiple geojsons. {z}, {x} and {y} are substituted | ||||||
|      * |      * | ||||||
|      * source: {overpassScript: "<custom overpass tags>"} when you want to do special things. _This should be really rare_. |      * source: {overpassScript: "<custom overpass tags>"} when you want to do special things. _This should be really rare_. | ||||||
|      *      This means that the data will be pulled from overpass with this script, and will ignore the osmTags for the query |      *      This means that the data will be pulled from overpass with this script, and will ignore the osmTags for the query | ||||||
|  | @ -40,7 +41,7 @@ export interface LayerConfigJson { | ||||||
|      *  While still supported, this is considered deprecated |      *  While still supported, this is considered deprecated | ||||||
|      */ |      */ | ||||||
|     source: { osmTags: AndOrTagConfigJson | string } | |     source: { osmTags: AndOrTagConfigJson | string } | | ||||||
|         { osmTags: AndOrTagConfigJson | string, geoJson: string } | |         { osmTags: AndOrTagConfigJson | string, geoJson: string, geoJsonZoomLevel?: number } | | ||||||
|         { osmTags: AndOrTagConfigJson | string, overpassScript: string } |         { osmTags: AndOrTagConfigJson | string, overpassScript: string } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  |  | ||||||
|  | @ -108,7 +108,7 @@ export default class LayoutConfig { | ||||||
|                     throw "Unkown fixed layer " + name; |                     throw "Unkown fixed layer " + name; | ||||||
|                 } |                 } | ||||||
|                 // @ts-ignore
 |                 // @ts-ignore
 | ||||||
|                 layer = Utils.Merge(layer.override, shared); |                 layer = Utils.Merge(layer.override, JSON.parse(JSON.stringify(shared))); // We make a deep copy of the shared layer, in order to protect it from changes
 | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             // @ts-ignore
 |             // @ts-ignore
 | ||||||
|  |  | ||||||
|  | @ -5,11 +5,13 @@ export default class SourceConfig { | ||||||
|     osmTags?: TagsFilter; |     osmTags?: TagsFilter; | ||||||
|     overpassScript?: string; |     overpassScript?: string; | ||||||
|     geojsonSource?: string; |     geojsonSource?: string; | ||||||
|  |     geojsonZoomLevel?: number; | ||||||
| 
 | 
 | ||||||
|     constructor(params: { |     constructor(params: { | ||||||
|         osmTags?: TagsFilter, |         osmTags?: TagsFilter, | ||||||
|         overpassScript?: string, |         overpassScript?: string, | ||||||
|         geojsonSource?: string |         geojsonSource?: string, | ||||||
|  |         geojsonSourceLevel?: number | ||||||
|     }) { |     }) { | ||||||
| 
 | 
 | ||||||
|         let defined = 0; |         let defined = 0; | ||||||
|  | @ -28,5 +30,6 @@ export default class SourceConfig { | ||||||
|         this.osmTags = params.osmTags; |         this.osmTags = params.osmTags; | ||||||
|         this.overpassScript = params.overpassScript; |         this.overpassScript = params.overpassScript; | ||||||
|         this.geojsonSource = params.geojsonSource; |         this.geojsonSource = params.geojsonSource; | ||||||
|  |         this.geojsonZoomLevel = params.geojsonSourceLevel; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | @ -33,14 +33,9 @@ export default class FeaturePipeline implements FeatureSource { | ||||||
|                                 updater) |                                 updater) | ||||||
|                         )), layout)); |                         )), layout)); | ||||||
| 
 | 
 | ||||||
|         const geojsonSources: GeoJsonSource [] = [] |         const geojsonSources: FeatureSource [] = GeoJsonSource | ||||||
|         for (const flayer of flayers.data) { |             .ConstructMultiSource(flayers.data, locationControl) | ||||||
|             const sourceUrl = flayer.layerDef.source.geojsonSource |             .map(geojsonSource => new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, geojsonSource))); | ||||||
|             if (sourceUrl !== undefined) { |  | ||||||
|                 geojsonSources.push(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, |  | ||||||
|                     new GeoJsonSource(flayer.layerDef.id, sourceUrl)))) |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|          |          | ||||||
|         const amendedLocalStorageSource = |         const amendedLocalStorageSource = | ||||||
|             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)) |             new RememberingSource(new RegisteringFeatureSource(new FeatureDuplicatorPerLayer(flayers, new LocalStorageSource(layout)) | ||||||
|  |  | ||||||
|  | @ -1,51 +1,195 @@ | ||||||
| import FeatureSource from "./FeatureSource"; | import FeatureSource from "./FeatureSource"; | ||||||
| import {UIEventSource} from "../UIEventSource"; | import {UIEventSource} from "../UIEventSource"; | ||||||
| import * as $ from "jquery"; | import * as $ from "jquery"; | ||||||
|  | import {control} from "leaflet"; | ||||||
|  | import zoom = control.zoom; | ||||||
|  | import Loc from "../../Models/Loc"; | ||||||
|  | import State from "../../State"; | ||||||
|  | import {Utils} from "../../Utils"; | ||||||
|  | import LayerConfig from "../../Customizations/JSON/LayerConfig"; | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Fetches a geojson file somewhere and passes it along |  * Fetches a geojson file somewhere and passes it along | ||||||
|  */ |  */ | ||||||
| export default class GeoJsonSource implements FeatureSource { | export default class GeoJsonSource implements FeatureSource { | ||||||
|  | 
 | ||||||
|     features: UIEventSource<{ feature: any; freshness: Date }[]>; |     features: UIEventSource<{ feature: any; freshness: Date }[]>; | ||||||
| 
 | 
 | ||||||
|     constructor(layerId: string, url: string, onFail: ((errorMsg: any) => void) = undefined) { |     private readonly onFail: ((errorMsg: any, url: string) => void) = undefined; | ||||||
|  | 
 | ||||||
|  |     private readonly layerId: string; | ||||||
|  | 
 | ||||||
|  |     private readonly seenids: Set<string> = new Set<string>() | ||||||
|  | 
 | ||||||
|  |     constructor(locationControl: UIEventSource<Loc>, | ||||||
|  |                 flayer: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }, | ||||||
|  |                 onFail?: ((errorMsg: any) => void)) { | ||||||
|  |         this.layerId = flayer.layerDef.id; | ||||||
|  |         let url = flayer.layerDef.source.geojsonSource; | ||||||
|  |         const zoomLevel = flayer.layerDef.source.geojsonZoomLevel; | ||||||
|  | 
 | ||||||
|  |         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>([]) | ||||||
|  | 
 | ||||||
|  |         if (zoomLevel === undefined) { | ||||||
|  |             // This is a classic, static geojson layer
 | ||||||
|             if (onFail === undefined) { |             if (onFail === undefined) { | ||||||
|                 onFail = errorMsg => { |                 onFail = errorMsg => { | ||||||
|                     console.warn(`Could not load geojson layer from`, url, "due to", errorMsg) |                     console.warn(`Could not load geojson layer from`, url, "due to", errorMsg) | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         this.features = new UIEventSource<{ feature: any; freshness: Date }[]>(undefined) |             this.onFail = onFail; | ||||||
|  | 
 | ||||||
|  |             this.LoadJSONFrom(url) | ||||||
|  |         } else { | ||||||
|  |             // This is a dynamic template with a fixed zoom level
 | ||||||
|  |             url = url.replace("{z}", "" + zoomLevel) | ||||||
|  |             const loadedTiles = new Set<string>(); | ||||||
|  |             const self = this; | ||||||
|  |             this.onFail = (msg, url) => { | ||||||
|  |                 console.warn(`Could not load geojson layer from`, url, "due to", msg) | ||||||
|  |                 loadedTiles.delete(url) | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const neededTiles = locationControl.map( | ||||||
|  |                 location => { | ||||||
|  | 
 | ||||||
|  |                     if (!flayer.isDisplayed.data) { | ||||||
|  |                         return undefined; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     // Yup, this is cheating to just get the bounds here
 | ||||||
|  |                     const bounds = State.state.leafletMap.data.getBounds() | ||||||
|  |                     const tileRange = Utils.TileRangeBetween(zoomLevel, bounds.getNorth(), bounds.getEast(), bounds.getSouth(), bounds.getWest()) | ||||||
|  |                     const needed = new Set<string>(); | ||||||
|  |                     for (let x = tileRange.xstart; x <= tileRange.xend; x++) { | ||||||
|  |                         for (let y = tileRange.ystart; y <= tileRange.yend; y++) { | ||||||
|  |                             let neededUrl = url.replace("{x}", "" + x).replace("{y}", "" + y); | ||||||
|  |                             needed.add(neededUrl) | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                     return needed; | ||||||
|  |                 } | ||||||
|  |             ); | ||||||
|  |             neededTiles.stabilized(250).addCallback((needed: Set<string>) => { | ||||||
|  |                 if (needed === undefined) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |                 needed.forEach(neededTile => { | ||||||
|  |                     if (loadedTiles.has(neededTile)) { | ||||||
|  |                         return; | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     loadedTiles.add(neededTile) | ||||||
|  |                     self.LoadJSONFrom(neededTile) | ||||||
|  | 
 | ||||||
|  |                 }) | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Merges together the layers which have the same source | ||||||
|  |      * @param flayers | ||||||
|  |      * @param locationControl | ||||||
|  |      * @constructor | ||||||
|  |      */ | ||||||
|  |     public static ConstructMultiSource(flayers: { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[], locationControl: UIEventSource<Loc>): GeoJsonSource[] { | ||||||
|  | 
 | ||||||
|  |         const flayersPerSource = new Map<string, { isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig }[]>(); | ||||||
|  |         for (const flayer of flayers) { | ||||||
|  |             const url = flayer.layerDef.source.geojsonSource | ||||||
|  |             if (url === undefined) { | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (!flayersPerSource.has(url)) { | ||||||
|  |                 flayersPerSource.set(url, []) | ||||||
|  |             } | ||||||
|  |             flayersPerSource.get(url).push(flayer) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         console.log("SOURCES", flayersPerSource) | ||||||
|  | 
 | ||||||
|  |         const sources: GeoJsonSource[] = [] | ||||||
|  | 
 | ||||||
|  |         flayersPerSource.forEach((flayers, key) => { | ||||||
|  |             if (flayers.length == 1) { | ||||||
|  |                 sources.push(new GeoJsonSource(locationControl, flayers[0])); | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const zoomlevels = Utils.Dedup(flayers.map(flayer => "" + (flayer.layerDef.source.geojsonZoomLevel ?? ""))) | ||||||
|  |             if (zoomlevels.length > 1) { | ||||||
|  |                 throw "Multiple zoomlevels defined for same geojson source " + key | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let isShown = new UIEventSource<boolean>(true, "IsShown for multiple layers: or of multiple values"); | ||||||
|  |             for (const flayer of flayers) { | ||||||
|  |                 flayer.isDisplayed.addCallbackAndRun(() => { | ||||||
|  |                     let value = false; | ||||||
|  |                     for (const flayer of flayers) { | ||||||
|  |                         value = flayer.isDisplayed.data || value; | ||||||
|  |                     } | ||||||
|  |                     isShown.setData(value); | ||||||
|  |                 }); | ||||||
|  | 
 | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const source = new GeoJsonSource(locationControl, { | ||||||
|  |                 isDisplayed: isShown, | ||||||
|  |                 layerDef: flayers[0].layerDef // We only care about the source info here
 | ||||||
|  |             }) | ||||||
|  |             sources.push(source) | ||||||
|  | 
 | ||||||
|  |         }) | ||||||
|  |         return sources; | ||||||
|  | 
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     private LoadJSONFrom(url: string) { | ||||||
|         const eventSource = this.features; |         const eventSource = this.features; | ||||||
|  |         const self = this; | ||||||
|         $.getJSON(url, function (json, status) { |         $.getJSON(url, function (json, status) { | ||||||
|             if (status !== "success") { |             if (status !== "success") { | ||||||
|                 console.log("Fetching geojson failed failed") |                 console.log("Fetching geojson failed failed") | ||||||
|                 onFail(status); |                 self.onFail(status, url); | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { |             if (json.elements === [] && json.remarks.indexOf("runtime error") > 0) { | ||||||
|                 console.log("Timeout or other runtime error"); |                 console.log("Timeout or other runtime error"); | ||||||
|                 onFail("Runtime error (timeout)") |                 self.onFail("Runtime error (timeout)", url) | ||||||
|                 return; |                 return; | ||||||
|             } |             } | ||||||
|             const time = new Date(); |             const time = new Date(); | ||||||
|             const features: { feature: any, freshness: Date } [] = [] |             const newFeatures: { feature: any, freshness: Date } [] = [] | ||||||
|             let i = 0; |             let i = 0; | ||||||
|  |             let skipped = 0; | ||||||
|             for (const feature of json.features) { |             for (const feature of json.features) { | ||||||
|                 if (feature.properties.id === undefined) { |                 if (feature.properties.id === undefined) { | ||||||
|                     feature.properties.id = url + "/" + i; |                     feature.properties.id = url + "/" + i; | ||||||
|                     feature.id = url + "/" + i; |                     feature.id = url + "/" + i; | ||||||
|                     i++; |                     i++; | ||||||
|                 } |                 } | ||||||
|                 feature._matching_layer_id = layerId; |                 if (self.seenids.has(feature.properties.id)) { | ||||||
|                 features.push({feature: feature, freshness: time}) |                     skipped++; | ||||||
|  |                     continue; | ||||||
|                 } |                 } | ||||||
|             console.log("Loaded features are", features) |                 self.seenids.add(feature.properties.id) | ||||||
|             eventSource.setData(features) |  | ||||||
| 
 | 
 | ||||||
|         }).fail(onFail) |                 newFeatures.push({feature: feature, freshness: time}) | ||||||
|  |             } | ||||||
|  |             console.log("Downloaded "+newFeatures.length+" new features and "+skipped+" already seen features from "+ url); | ||||||
|              |              | ||||||
|  |             if(newFeatures.length == 0){ | ||||||
|  |                 return; | ||||||
|             } |             } | ||||||
|              |              | ||||||
|  |             eventSource.setData(eventSource.data.concat(newFeatures)) | ||||||
|  | 
 | ||||||
|  |         }).fail(msg => self.onFail(msg, url)) | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | @ -21,8 +21,14 @@ export default class ExtractRelations { | ||||||
|         State.state.knownRelations.setData(memberships) |         State.state.knownRelations.setData(memberships) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private static GetRelationElements(overpassJson: any): Relation[] { |     /** | ||||||
|         const relations = overpassJson.elements.filter(element => element.type === "relation") |      * Gets an overview of the relations - except for multipolygons. We don't care about those | ||||||
|  |      * @param overpassJson | ||||||
|  |      * @constructor | ||||||
|  |      */ | ||||||
|  |     public static GetRelationElements(overpassJson: any): Relation[] { | ||||||
|  |         const relations = overpassJson.elements | ||||||
|  |             .filter(element => element.type === "relation" && element.tags.type !== "multipolygon") | ||||||
|         for (const relation of relations) { |         for (const relation of relations) { | ||||||
|             relation.properties = relation.tags |             relation.properties = relation.tags | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  | @ -48,7 +48,7 @@ export class Overpass { | ||||||
|             }).fail(onFail) |             }).fail(onFail) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     private buildQuery(bbox: string): string { |     buildQuery(bbox: string): string { | ||||||
|         const filters = this._filter.asOverpass() |         const filters = this._filter.asOverpass() | ||||||
|         let filter = "" |         let filter = "" | ||||||
|         for (const filterOr of filters) { |         for (const filterOr of filters) { | ||||||
|  |  | ||||||
							
								
								
									
										32
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										32
									
								
								Utils.ts
									
										
									
									
									
								
							|  | @ -156,8 +156,6 @@ export class Utils { | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     static Merge(source: any, target: any) { |     static Merge(source: any, target: any) { | ||||||
|         target = JSON.parse(JSON.stringify(target)); |  | ||||||
|         source = JSON.parse(JSON.stringify(source)); |  | ||||||
|         for (const key in source) { |         for (const key in source) { | ||||||
|             const sourceV = source[key]; |             const sourceV = source[key]; | ||||||
|             const targetV = target[key] |             const targetV = target[key] | ||||||
|  | @ -204,6 +202,26 @@ export class Utils { | ||||||
|         return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z} |         return {x: Utils.lon2tile(lon, z), y: Utils.lat2tile(lat, z), z: z} | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     static TileRangeBetween(zoomlevel: number, lat0: number, lon0: number, lat1:number, lon1: number) : TileRange{ | ||||||
|  |         const t0 = Utils.embedded_tile(lat0, lon0, zoomlevel) | ||||||
|  |         const t1 = Utils.embedded_tile(lat1, lon1, zoomlevel) | ||||||
|  | 
 | ||||||
|  |         const xstart = Math.min(t0.x, t1.x) | ||||||
|  |         const xend = Math.max(t0.x, t1.x) | ||||||
|  |         const ystart = Math.min(t0.y, t1.y) | ||||||
|  |         const yend = Math.max(t0.y, t1.y) | ||||||
|  |         const total = (1 + xend - xstart) * (1 + yend - ystart) | ||||||
|  |          | ||||||
|  |         return { | ||||||
|  |             xstart: xstart, | ||||||
|  |             xend: xend, | ||||||
|  |             ystart: ystart, | ||||||
|  |             yend: yend,  | ||||||
|  |             total: total, | ||||||
|  |             zoomlevel: zoomlevel | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     public static MinifyJSON(stringified: string): string { |     public static MinifyJSON(stringified: string): string { | ||||||
|         stringified = stringified.replace(/\|/g, "||"); |         stringified = stringified.replace(/\|/g, "||"); | ||||||
| 
 | 
 | ||||||
|  | @ -257,3 +275,13 @@ export class Utils { | ||||||
|         return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom))); |         return (Math.floor((1 - Math.log(Math.tan(lat * Math.PI / 180) + 1 / Math.cos(lat * Math.PI / 180)) / Math.PI) / 2 * Math.pow(2, zoom))); | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | export interface TileRange{ | ||||||
|  |     xstart: number, | ||||||
|  |     ystart: number, | ||||||
|  |     xend: number, | ||||||
|  |     yend: number, | ||||||
|  |     total: number, | ||||||
|  |     zoomlevel: number | ||||||
|  | } | ||||||
|  | @ -24,19 +24,64 @@ | ||||||
|   "socialImage": "", |   "socialImage": "", | ||||||
|   "defaultBackgroundId": "CartoDB.Positron", |   "defaultBackgroundId": "CartoDB.Positron", | ||||||
|   "layers": [ |   "layers": [ | ||||||
|     "play_forest", |     { | ||||||
|     "playground", |       "builtin": "play_forest", | ||||||
|     "sport_pitch", |       "override": { | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 14 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "builtin": "playground", | ||||||
|  |       "override": { | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 14 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "builtin": "sport_pitch", | ||||||
|  |       "override": { | ||||||
|  |         "minzoom": 15, | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 14 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       "builtin": "slow_roads", |       "builtin": "slow_roads", | ||||||
|       "override": { |       "override": { | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 14 | ||||||
|  |         }, | ||||||
|         "calculatedTags": [ |         "calculatedTags": [ | ||||||
|           "_part_of_walking_routes=feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\").join(', ')" |           "_part_of_walking_routes=feat.memberships().map(r => \"<a href='#relation/\"+r.relation.id+\"'>\" + r.relation.tags.name + \"</a>\").join(', ')" | ||||||
|         ] |         ] | ||||||
|       } |       } | ||||||
|     }, |     }, | ||||||
|     "grass_in_parks", |     { | ||||||
|     "village_green", |       "builtin": "grass_in_parks", | ||||||
|  |       "override": { | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 14 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "builtin": "village_green", | ||||||
|  |       "override": { | ||||||
|  |         "source": { | ||||||
|  |           "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |           "geoJsonZoomLevel": 14 | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|     { |     { | ||||||
|       "id": "walking_routes", |       "id": "walking_routes", | ||||||
|       "name": { |       "name": { | ||||||
|  | @ -50,7 +95,9 @@ | ||||||
|             "route=foot", |             "route=foot", | ||||||
|             "operator=provincie Antwerpen" |             "operator=provincie Antwerpen" | ||||||
|           ] |           ] | ||||||
|         } |         }, | ||||||
|  |         "geoJson": "https://pietervdvn.github.io/speelplekken_cache/speelplekken_{z}_{x}_{y}.geojson", | ||||||
|  |         "geoJsonZoomLevel": 14 | ||||||
|       }, |       }, | ||||||
|       "title": { |       "title": { | ||||||
|         "render": "Wandeling <i>{name}</i>", |         "render": "Wandeling <i>{name}</i>", | ||||||
|  | @ -141,17 +188,6 @@ | ||||||
|       "width": { |       "width": { | ||||||
|         "render": "3" |         "render": "3" | ||||||
|       } |       } | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|       "id": "speelplekken-cache", |  | ||||||
|       "name": "", |  | ||||||
|       "source": { |  | ||||||
|         "osmTags": { |  | ||||||
|           "or": [] |  | ||||||
|         }, |  | ||||||
|         "geoJson": "https://pietervdvn.github.io/speelplekken-cache.geojson" |  | ||||||
|       }, |  | ||||||
|       "passAllFeatures": true |  | ||||||
|     } |     } | ||||||
|   ], |   ], | ||||||
|   "roamingRenderings": [ |   "roamingRenderings": [ | ||||||
|  |  | ||||||
|  | @ -9,12 +9,13 @@ | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", |     "increase-memory": "export NODE_OPTIONS=--max_old_space_size=4096", | ||||||
|     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", |     "start": "ts-node scripts/generateLayerOverview.ts --no-fail && npm run increase-memory && parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", | ||||||
|     "test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts && ts-node test/ImageAttribution.spec.ts", |     "test": "ts-node test/Tag.spec.ts && ts-node test/TagQuestion.spec.ts && ts-node test/ImageSearcher.spec.ts && ts-node test/ImageAttribution.spec.ts && ts-node test/Theme.spec.ts", | ||||||
|     "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", |     "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", | ||||||
|     "generate:images": "ts-node scripts/generateIncludedImages.ts", |     "generate:images": "ts-node scripts/generateIncludedImages.ts", | ||||||
|     "generate:translations": "ts-node scripts/generateTranslations.ts", |     "generate:translations": "ts-node scripts/generateTranslations.ts", | ||||||
|     "generate:layouts": "ts-node scripts/generateLayouts.ts", |     "generate:layouts": "ts-node scripts/generateLayouts.ts", | ||||||
|     "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", |     "generate:docs": "ts-node scripts/generateDocs.ts && ts-node scripts/generateTaginfoProjectFiles.ts", | ||||||
|  |     "generate:cache:speelplekken": "ts-node scripts/generateCache.ts speelplekken 14 ./cache/speelplekken 51.2003 4.3925 51.1058 4.5087", | ||||||
|     "generate:layeroverview": "ts-node scripts/generateLayerOverview.ts --no-fail", |     "generate:layeroverview": "ts-node scripts/generateLayerOverview.ts --no-fail", | ||||||
|     "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", |     "generate:licenses": "ts-node scripts/generateLicenseInfo.ts --no-fail", | ||||||
|     "validate:layeroverview": "ts-node scripts/generateLayerOverview.ts --report", |     "validate:layeroverview": "ts-node scripts/generateLayerOverview.ts --report", | ||||||
|  |  | ||||||
|  | @ -1,4 +1,5 @@ | ||||||
| import {lstatSync, readdirSync} from "fs"; | import {lstatSync, readdirSync} from "fs"; | ||||||
|  | import * as https from "https"; | ||||||
| 
 | 
 | ||||||
| export default class ScriptUtils { | export default class ScriptUtils { | ||||||
|     public static readDirRecSync(path): string[] { |     public static readDirRecSync(path): string[] { | ||||||
|  | @ -17,4 +18,29 @@ export default class ScriptUtils { | ||||||
|         return result; |         return result; | ||||||
|     } |     } | ||||||
|      |      | ||||||
|  |     public static DownloadJSON(url, continuation : (parts : string []) => void){ | ||||||
|  |         https.get(url, (res) => { | ||||||
|  |             console.log("Got response!") | ||||||
|  |             const parts : string[] = [] | ||||||
|  |             res.setEncoding('utf8'); | ||||||
|  |             res.on('data', function (chunk) { | ||||||
|  |                 // @ts-ignore
 | ||||||
|  |                 parts.push(chunk) | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             res.addListener('end', function () { | ||||||
|  |                 continuation(parts) | ||||||
|  |             }); | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     public static sleep(ms) { | ||||||
|  |         return new Promise((resolve) => { | ||||||
|  |             console.debug("Sleeping for", ms) | ||||||
|  |             setTimeout(resolve, ms); | ||||||
|  |             | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,21 +0,0 @@ | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| // Loads a geojson file downloaded from overpass, renames "@id" to "id" and deletes "@relations"
 |  | ||||||
| 
 |  | ||||||
| import {readFileSync, writeFileSync} from "fs"; |  | ||||||
| 
 |  | ||||||
| const source = process.argv[2] ?? "~/Downloads/export.json" |  | ||||||
| console.log("Fixing up ", source) |  | ||||||
| const contents = readFileSync(source, "UTF8"); |  | ||||||
| const f = JSON.parse(contents); |  | ||||||
| let i = 0 |  | ||||||
| for (const feature of f.features) { |  | ||||||
|     if(feature.properties == undefined){ |  | ||||||
|         continue |  | ||||||
|     } |  | ||||||
|     feature.properties["id"] = feature.properties["@id"] |  | ||||||
|     feature.properties["@id"] = undefined |  | ||||||
|     feature.properties["@relations"] = undefined |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| writeFileSync(source+".fixed", JSON.stringify(f, null, "  ")) |  | ||||||
							
								
								
									
										176
									
								
								scripts/generateCache.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								scripts/generateCache.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,176 @@ | ||||||
|  | /** | ||||||
|  |  * Generates a collection of geojson files based on an overpass query for a given theme | ||||||
|  |  */ | ||||||
|  | import {TileRange, Utils} from "../Utils"; | ||||||
|  | 
 | ||||||
|  | Utils.runningFromConsole = true | ||||||
|  | import {Overpass} from "../Logic/Osm/Overpass"; | ||||||
|  | import {writeFileSync, existsSync, readFileSync} from "fs"; | ||||||
|  | import {TagsFilter} from "../Logic/Tags/TagsFilter"; | ||||||
|  | import {Or} from "../Logic/Tags/Or"; | ||||||
|  | import LayoutConfig from "../Customizations/JSON/LayoutConfig"; | ||||||
|  | import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; | ||||||
|  | import ScriptUtils from "./ScriptUtils"; | ||||||
|  | import ExtractRelations from "../Logic/Osm/ExtractRelations"; | ||||||
|  | import * as OsmToGeoJson from "osmtogeojson"; | ||||||
|  | import {Script} from "vm"; | ||||||
|  | 
 | ||||||
|  | function createOverpassObject(theme: LayoutConfig) { | ||||||
|  |     let filters: TagsFilter[] = []; | ||||||
|  |     let extraScripts: string[] = []; | ||||||
|  |     for (const layer of theme.layers) { | ||||||
|  |         if (typeof (layer) === "string") { | ||||||
|  |             throw "A layer was not expanded!" | ||||||
|  |         } | ||||||
|  |         if (layer.doNotDownload) { | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  |         if (layer.source.geojsonSource !== undefined) { | ||||||
|  |             // Not our responsibility to download this layer!
 | ||||||
|  |             continue; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |         // Check if data for this layer has already been loaded
 | ||||||
|  |         if (layer.source.overpassScript !== undefined) { | ||||||
|  |             extraScripts.push(layer.source.overpassScript) | ||||||
|  |         } else { | ||||||
|  |             filters.push(layer.source.osmTags); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  |     filters = Utils.NoNull(filters) | ||||||
|  |     extraScripts = Utils.NoNull(extraScripts) | ||||||
|  |     if (filters.length + extraScripts.length === 0) { | ||||||
|  |         throw "Nothing to download! The theme doesn't declare anything to download" | ||||||
|  |     } | ||||||
|  |     return new Overpass(new Or(filters), extraScripts); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function saveResponse(chunks: string[], targetDir: string) { | ||||||
|  |     const contents = chunks.join("") | ||||||
|  |     if (contents.startsWith("<?xml")) { | ||||||
|  |         // THis is an error message
 | ||||||
|  |         console.error("Failed to create ", targetDir, "probably over quota") | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     writeFileSync(targetDir, contents) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function rawJsonName(targetDir: string, x: number, y: number, z: number): string { | ||||||
|  |     return targetDir + "_" + z + "_" + x + "_" + y + ".json" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function geoJsonName(targetDir: string, x: number, y: number, z: number): string { | ||||||
|  |     return targetDir + "_" + z + "_" + x + "_" + y + ".geojson" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function metaJsonName(targetDir: string, x: number, y: number, z: number): string { | ||||||
|  |     return targetDir + "_" + z + "_" + x + "_" + y + ".meta.json" | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function downloadRaw(targetdir: string,  r: TileRange, overpass: Overpass) { | ||||||
|  |     let downloaded = 0 | ||||||
|  |     for (let x = r.xstart; x <= r.xend; x++) { | ||||||
|  |         for (let y = r.ystart; y <= r.yend; y++) { | ||||||
|  |             console.log("x:", (x - r.xstart), "/", (r.xend - r.xstart), "; y:", (y - r.ystart), "/", (r.yend - r.ystart), "; total: ", downloaded, "/", r.total) | ||||||
|  |             downloaded++; | ||||||
|  |             const filename = rawJsonName(targetdir, x, y, r.zoomlevel) | ||||||
|  |             if (existsSync(filename)) { | ||||||
|  |                 console.log("Already exists: ", filename) | ||||||
|  |                 continue; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const boundsArr = Utils.tile_bounds(r.zoomlevel, x, y) | ||||||
|  |             const bounds = { | ||||||
|  |                 north: Math.max(boundsArr[0][0], boundsArr[1][0]), | ||||||
|  |                 south: Math.min(boundsArr[0][0], boundsArr[1][0]), | ||||||
|  |                 east: Math.max(boundsArr[0][1], boundsArr[1][1]), | ||||||
|  |                 west: Math.min(boundsArr[0][1], boundsArr[1][1]) | ||||||
|  |             } | ||||||
|  |             console.log("Downloading tile", r.zoomlevel, x, y, "with bounds", bounds) | ||||||
|  |             const url = overpass.buildQuery("[bbox:" + bounds.south + "," + bounds.west + "," + bounds.north + "," + bounds.east + "]") | ||||||
|  | 
 | ||||||
|  |             ScriptUtils.DownloadJSON(url, | ||||||
|  |                 chunks => { | ||||||
|  |                     saveResponse(chunks, filename) | ||||||
|  |                 }) | ||||||
|  | 
 | ||||||
|  |             await ScriptUtils.sleep(10000) | ||||||
|  |             console.debug("Waking up") | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function postProcess(targetdir: string, r: TileRange) { | ||||||
|  |     let processed = 0; | ||||||
|  |     for (let x = r.xstart; x <= r.xend; x++) { | ||||||
|  |         for (let y = r.ystart; y <= r.yend; y++) { | ||||||
|  |             processed++; | ||||||
|  |             const filename = rawJsonName(targetdir, x, y, r.zoomlevel) | ||||||
|  |             console.log(" Post processing", processed, "/",r. total, filename) | ||||||
|  |             if (!existsSync(filename)) { | ||||||
|  |                 throw "Not found - and not downloaded. Run this script again!: " + filename | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // We read the raw OSM-file and convert it to a geojson
 | ||||||
|  |             const rawOsm = JSON.parse(readFileSync(filename, "UTF8")) | ||||||
|  | 
 | ||||||
|  |             // Create and save the geojson file - which is the main chunk of the data
 | ||||||
|  |             const geojson = OsmToGeoJson.default(rawOsm); | ||||||
|  |             writeFileSync(geoJsonName(targetdir, x, y, r.zoomlevel), JSON.stringify(geojson)) | ||||||
|  | 
 | ||||||
|  |             // Extract the relationship information
 | ||||||
|  |             const relations = ExtractRelations.GetRelationElements(rawOsm) | ||||||
|  |             const osmTime = new Date(rawOsm.osm3s.timestamp_osm_base); | ||||||
|  | 
 | ||||||
|  |             const meta = { | ||||||
|  |                 freshness: osmTime, | ||||||
|  |                 relations: relations | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             writeFileSync( | ||||||
|  |                 metaJsonName(targetdir, x, y, r.zoomlevel), | ||||||
|  |                 JSON.stringify(meta) | ||||||
|  |             ) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function main(args: string[]) { | ||||||
|  | 
 | ||||||
|  |     if (args.length == 0) { | ||||||
|  |         console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1") | ||||||
|  |         return; | ||||||
|  |     } | ||||||
|  |     const themeName = args[0] | ||||||
|  |     const zoomlevel = Number(args[1]) | ||||||
|  |     const targetdir = args[2] | ||||||
|  |     const lat0 = Number(args[3]) | ||||||
|  |     const lon0 = Number(args[4]) | ||||||
|  |     const lat1 = Number(args[5]) | ||||||
|  |     const lon1 = Number(args[6]) | ||||||
|  | 
 | ||||||
|  |     const tileRange = Utils.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) | ||||||
|  | 
 | ||||||
|  |     const theme = AllKnownLayouts.allKnownLayouts.get(themeName) | ||||||
|  |     if (theme === undefined) { | ||||||
|  |         const keys = [] | ||||||
|  |         AllKnownLayouts.allKnownLayouts.forEach((_, key) => { | ||||||
|  |             keys.push(key) | ||||||
|  |         }) | ||||||
|  |         console.error("The theme " + theme + " was not found; try one of ", keys); | ||||||
|  |         return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const overpass = createOverpassObject(theme) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     await downloadRaw(targetdir, tileRange, overpass) | ||||||
|  |     await postProcess(targetdir, tileRange) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | let args = [...process.argv] | ||||||
|  | args.splice(0, 2) | ||||||
|  | main(args); | ||||||
							
								
								
									
										48
									
								
								test/Theme.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								test/Theme.spec.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,48 @@ | ||||||
|  | import T from "./TestHelper"; | ||||||
|  | import {Utils} from "../Utils"; | ||||||
|  | 
 | ||||||
|  | Utils.runningFromConsole = true; | ||||||
|  | import TagRenderingQuestion from "../UI/Popup/TagRenderingQuestion"; | ||||||
|  | import {UIEventSource} from "../Logic/UIEventSource"; | ||||||
|  | import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig"; | ||||||
|  | import LayoutConfig from "../Customizations/JSON/LayoutConfig"; | ||||||
|  | import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; | ||||||
|  | import * as assert from "assert"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | new T("Theme tests", | ||||||
|  |     [ | ||||||
|  |         ["Nested overrides work", () => { | ||||||
|  |      | ||||||
|  |             const themeConfigJson : LayoutConfigJson = { | ||||||
|  |                 description: "Descr", | ||||||
|  |                 icon: "", | ||||||
|  |                 language: ["en"], | ||||||
|  |                 layers: [ | ||||||
|  |                     { | ||||||
|  |                         builtin: "public_bookcase", | ||||||
|  |                         override: { | ||||||
|  |                             source:{ | ||||||
|  |                                 geoJson: "xyz" | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 ], | ||||||
|  |                 maintainer: "", | ||||||
|  |                 startLat: 0, | ||||||
|  |                 startLon: 0, | ||||||
|  |                 startZoom: 0, | ||||||
|  |                 title: { | ||||||
|  |                     en: "Title" | ||||||
|  |                 }, | ||||||
|  |                 version: "", | ||||||
|  |                 id: "test" | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             const themeConfig = new LayoutConfig(themeConfigJson); | ||||||
|  |             assert.equal("xyz", themeConfig.layers[0].source.geojsonSource) | ||||||
|  |      | ||||||
|  |              | ||||||
|  |         }] | ||||||
|  |     ] | ||||||
|  | ); | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue