diff --git a/Logic/BBox.ts b/Logic/BBox.ts index 436f00125..7289b793b 100644 --- a/Logic/BBox.ts +++ b/Logic/BBox.ts @@ -10,6 +10,10 @@ export class BBox { readonly minLat: number; readonly minLon: number; + /*** + * Coordinates should be [[lon, lat],[lon, lat]] + * @param coordinates + */ constructor(coordinates) { this.maxLat = -90; this.maxLon = -180; @@ -44,6 +48,21 @@ export class BBox { } return feature.bbox; } + + static bboxAroundAll(bboxes: BBox[]): BBox{ + let maxLat: number = -90; + let maxLon: number= -180; + let minLat: number= 80; + let minLon: number= 180; + + for (const bbox of bboxes) { + maxLat = Math.max(maxLat, bbox.maxLat) + maxLon = Math.max(maxLon, bbox.maxLon) + minLat = Math.min(minLat, bbox.minLat) + minLon = Math.min(minLon, bbox.minLon) + } + return new BBox([[maxLon, maxLat],[minLon,minLat]]) + } static fromTile(z: number, x: number, y: number): BBox { return new BBox(Tiles.tile_bounds_lon_lat(z, x, y)) diff --git a/Logic/ExtraFunctions.ts b/Logic/ExtraFunctions.ts index b2fb6df92..6e8ca9bca 100644 --- a/Logic/ExtraFunctions.ts +++ b/Logic/ExtraFunctions.ts @@ -14,6 +14,7 @@ export interface ExtraFuncParams { */ getFeaturesWithin: (layerId: string, bbox: BBox) => any[][], memberships: RelationsTracker + getFeatureById: (id:string) => any } /** @@ -79,20 +80,19 @@ class DistanceToFunc implements ExtraFunction { } if (typeof arg0 === "number") { // Feature._lon and ._lat is conveniently place by one of the other metatags - return GeoOperations.distanceBetween([arg0, lat], [feature._lon, feature._lat]); + return GeoOperations.distanceBetween([arg0, lat], GeoOperations.centerpointCoordinates(feature)); } if (typeof arg0 === "string") { // This is an identifier - // TODO FIXME - const feature = undefined // State.state.allElements.ContainingFeatures.get(arg0); + const feature = featuresPerLayer.getFeatureById(arg0) if (feature === undefined) { return undefined; } arg0 = feature; } - // arg0 is probably a feature - return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), [feature._lon, feature._lat]) + // arg0 is probably a geojsonfeature + return GeoOperations.distanceBetween(GeoOperations.centerpointCoordinates(arg0), GeoOperations.centerpointCoordinates(feature)) } } diff --git a/Logic/FeatureSource/FeaturePipeline.ts b/Logic/FeatureSource/FeaturePipeline.ts index 32e7ed0ae..9e8a0afa7 100644 --- a/Logic/FeatureSource/FeaturePipeline.ts +++ b/Logic/FeatureSource/FeaturePipeline.ts @@ -448,11 +448,12 @@ export default class FeaturePipeline { window.setTimeout( () => { const layerDef = src.layer.layerDef; - const somethingChanged = MetaTagging.addMetatags( + MetaTagging.addMetatags( src.features.data, { memberships: this.relationTracker, - getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox) + getFeaturesWithin: (layerId, bbox: BBox) => self.GetFeaturesWithin(layerId, bbox), + getFeatureById: (id:string) => self.state.allElements.ContainingFeatures.get(id) }, layerDef, { diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index 84bb8dbe8..42608e202 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -178,8 +178,6 @@ export default class MetaTagging { try { const functions = MetaTagging.createFunctionsForFeature(layer.id, calculatedTags) - - ExtraFunctions.FullPatchFeature(params, feature); for (const f of functions) { f(feature); diff --git a/Logic/State/FeaturePipelineState.ts b/Logic/State/FeaturePipelineState.ts index 96a29beb6..7322fb59f 100644 --- a/Logic/State/FeaturePipelineState.ts +++ b/Logic/State/FeaturePipelineState.ts @@ -8,6 +8,7 @@ import {UIEventSource} from "../UIEventSource"; import MapState from "./MapState"; import SelectedFeatureHandler from "../Actors/SelectedFeatureHandler"; import Hash from "../Web/Hash"; +import {BBox} from "../BBox"; export default class FeaturePipelineState extends MapState { @@ -29,6 +30,8 @@ export default class FeaturePipelineState extends MapState { clusterCounter.addTile(source) + const sourceBBox = source.features.map(allFeatures => BBox.bboxAroundAll(allFeatures.map(f => BBox.get(f.feature)))) + // Do show features indicates if the 'showDataLayer' should be shown const doShowFeatures = source.features.map( f => { @@ -44,7 +47,7 @@ export default class FeaturePipelineState extends MapState { return false; } - if (!source.bbox.overlapsWith(bounds)) { + if (!sourceBBox.data.overlapsWith(bounds)) { // Not within range -> features are hidden return false } @@ -81,7 +84,7 @@ export default class FeaturePipelineState extends MapState { return true - }, [this.currentBounds, source.layer.isDisplayed] + }, [this.currentBounds, source.layer.isDisplayed, sourceBBox] ) new ShowDataLayer( diff --git a/Models/Constants.ts b/Models/Constants.ts index 13c2dab46..06ba69e91 100644 --- a/Models/Constants.ts +++ b/Models/Constants.ts @@ -2,7 +2,7 @@ import {Utils} from "../Utils"; export default class Constants { - public static vNumber = "0.13.0-alpha-2"; + public static vNumber = "0.13.0-alpha-3"; public static ImgurApiKey = '7070e7167f0a25a' public static readonly mapillary_client_token_v4 = "MLY|4441509239301885|b40ad2d3ea105435bd40c7e76993ae85" diff --git a/Models/ThemeConfig/DependencyCalculator.ts b/Models/ThemeConfig/DependencyCalculator.ts index 60903dd5f..93a3ed131 100644 --- a/Models/ThemeConfig/DependencyCalculator.ts +++ b/Models/ThemeConfig/DependencyCalculator.ts @@ -71,6 +71,7 @@ export default class DependencyCalculator { let currentKey = undefined let currentLine = undefined const params: ExtraFuncParams = { + getFeatureById: _ => undefined, getFeaturesWithin: (layerId, _) => { if(layerId === '*'){ diff --git a/Models/ThemeConfig/Json/LayoutConfigJson.ts b/Models/ThemeConfig/Json/LayoutConfigJson.ts index 27d10823e..79742b2fb 100644 --- a/Models/ThemeConfig/Json/LayoutConfigJson.ts +++ b/Models/ThemeConfig/Json/LayoutConfigJson.ts @@ -219,7 +219,7 @@ export interface LayoutConfigJson { * If clustering is defined, defaults to 25 */ minNeededElements?: number - }, + } | false, /** * The URL of a custom CSS stylesheet to modify the layout diff --git a/Models/ThemeConfig/SourceConfig.ts b/Models/ThemeConfig/SourceConfig.ts index 7cb58124b..19afbcad1 100644 --- a/Models/ThemeConfig/SourceConfig.ts +++ b/Models/ThemeConfig/SourceConfig.ts @@ -5,9 +5,9 @@ export default class SourceConfig { public readonly osmTags?: TagsFilter; public readonly overpassScript?: string; - public readonly geojsonSource?: string; - public readonly geojsonZoomLevel?: number; - public readonly isOsmCacheLayer: boolean; + public geojsonSource?: string; + public geojsonZoomLevel?: number; + public isOsmCacheLayer: boolean; public readonly mercatorCrs: boolean; constructor(params: { diff --git a/Utils.ts b/Utils.ts index bd9cfda2a..e0c9cd57a 100644 --- a/Utils.ts +++ b/Utils.ts @@ -212,7 +212,7 @@ Note that these values can be prepare with javascript in the theme by using a [c } if(v.InnerConstructElement !== undefined){ - console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string", v) + console.warn("SubstituteKeys received a BaseUIElement to substitute in - this is probably a bug and will be downcast to a string\nThe key is", key,"\nThe value is", v) v = ( v.InnerConstructElement())?.innerText } diff --git a/assets/themes/postal_codes/license_info.json b/assets/themes/postal_codes/license_info.json new file mode 100644 index 000000000..31eca5e79 --- /dev/null +++ b/assets/themes/postal_codes/license_info.json @@ -0,0 +1,12 @@ +[ + { + "path": "townhall.svg", + "license": "CC0", + "authors": [ + "Nebulon42" + ], + "sources": [ + "https://wiki.openstreetmap.org/wiki/File:Town-hall-16.svg" + ] + } +] \ No newline at end of file diff --git a/assets/themes/postal_codes.json b/assets/themes/postal_codes/postal_codes.json similarity index 68% rename from assets/themes/postal_codes.json rename to assets/themes/postal_codes/postal_codes.json index 719878936..06b3e0496 100644 --- a/assets/themes/postal_codes.json +++ b/assets/themes/postal_codes/postal_codes.json @@ -13,7 +13,7 @@ "en" ], "maintainer": "", - "icon": "./assets/svg/bug.svg", + "icon": "./assets/themes/postal_codes/townhall.svg", "version": "0", "startLat": 0, "startLon": 0, @@ -21,13 +21,15 @@ "widenFactor": 0.05, "socialImage": "", "hideFromOverview": true, + "clustering": false, + "overpassTimeout": 180, "layers": [ { - "id": "postal_codes", + "id": "postal_code_boundary", "name": { "en": "postal codes" }, - "minzoom": 12, + "minzoom": 8, "title": { "render": { "en": "Postal code {postal_code}" @@ -42,11 +44,7 @@ } } ], - "presets": [], "source": { - "isOsmCache": true, - "geoJson": "http://127.0.0.1:8080/postal_codes_postal_codes_{z}_{x}_{y}.geojson", - "geoJsonZoomLevel": 1, "osmTags": { "or": [ "boundary=postal_code", @@ -61,12 +59,7 @@ }, "mapRendering": [ { - "icon": { - "render": "./assets/svg/bug.svg" - }, - "iconSize": { - "render": "40,40,center" - }, + "label": "
{postal_code}
", "location": [ "point", "centroid" @@ -77,8 +70,10 @@ "render": "#00f" }, "width": { - "render": "8" - } + "render": "4" + }, + "fill": "no", + "dashArray": "8 8" } ], "isShown": { @@ -90,34 +85,30 @@ } }, { - "id": "town_halls", + "id": "town_hall", "name": { "en": "town halls" }, "minzoom": 12, "title": { "render": { - "en": "Town halls" + "en": "Town hall {name}" } }, "calculatedTags": [ - "_postal_code=feat.overlapWith('postal_codes')[0]?.feat?.properties?.postal_code" - ], + "_postal_code_properties=(() => { const f = feat.overlapWith('postal_code_boundary'); if(f.length===0){return {};}; const p = f[0]?.feat?.properties; return {id:p.id, postal_code: p.postal_code, _closest_town_hall: p._closest_town_hall}; })()", + "_postal_code=feat.get('_postal_code_properties')?.postal_code", + "_postal_code_center_distance=feat.distanceTo(feat.get('_postal_code_properties').id)" + ], "description": {}, "tagRenderings": [ ], "presets": [], "source": { - "isOsmCache": true, - "geoJson": "http://127.0.0.1:8080/postal_codes_town_hall_{z}_{x}_{y}.geojson", - "geoJsonZoomLevel": 1, "osmTags": "amenity=townhall" }, "mapRendering": [ - { - "icon": { - "render": "./assets/svg/bug.svg" - }, + { "icon": "./assets/themes/postal_codes/townhall.svg", "iconSize": { "render": "40,40,center" }, @@ -125,14 +116,6 @@ "point", "centroid" ] - }, - { - "color": { - "render": "#00f" - }, - "width": { - "render": "8" - } } ], "isShown": { diff --git a/assets/themes/postal_codes/townhall.svg b/assets/themes/postal_codes/townhall.svg new file mode 100644 index 000000000..7fe78288b --- /dev/null +++ b/assets/themes/postal_codes/townhall.svg @@ -0,0 +1,25 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/scripts/generateCache.ts b/scripts/generateCache.ts index 6c4d167ad..3408c8008 100644 --- a/scripts/generateCache.ts +++ b/scripts/generateCache.ts @@ -24,7 +24,6 @@ import {GeoOperations} from "../Logic/GeoOperations"; import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"; import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"; import Loc from "../Models/Loc"; - ScriptUtils.fixUtils() @@ -181,6 +180,23 @@ function loadAllTiles(targetdir: string, r: TileRange, theme: LayoutConfig, extr function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relationsTracker: RelationsTracker, targetdir: string, pointsOnlyLayers: string[]) { const skippedLayers = new Set() + const indexedFeatures : Map = new Map() + let indexisBuilt = false; + function buildIndex(){ + for (const ff of allFeatures.features.data) { + const f = ff.feature + indexedFeatures.set(f.properties.id, f) + } + indexisBuilt = true; + } + + function getFeatureById(id){ + if(!indexisBuilt){ + buildIndex() + } + return indexedFeatures.get(id) + } + async function handleLayer(source: FeatureSourceForLayer) { const layer = source.layer.layerDef; const targetZoomLevel = layer.source.geojsonZoomLevel ?? 0 @@ -199,7 +215,8 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations memberships: relationsTracker, getFeaturesWithin: _ => { return [allFeatures.features.data.map(f => f.feature)] - } + }, + getFeatureById: getFeatureById }, layer, { @@ -237,6 +254,7 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations new UIEventSource(undefined) ) + console.log("Tile "+layer.id+"."+tileIndex+" contains "+filteredTile.features.data.length+" features after filtering ("+tile.features.data.length+") features before") if (filteredTile.features.data.length === 0) { return } @@ -252,7 +270,7 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations const calculatedTagKeys = tile.layer.layerDef.calculatedTags.map(ct => ct[0]) featureCount++ for (const calculatedTagKey of calculatedTagKeys) { - const strict = feature.feature.properties[calculatedTagKey] + const strict = feature.feature.properties[calculatedTagKey] feature.feature.properties[calculatedTagKey] =strict strictlyCalculated ++; if(strictlyCalculated % 100 === 0){ @@ -292,7 +310,18 @@ function sliceToTiles(allFeatures: FeatureSource, theme: LayoutConfig, relations // And, if needed, to create a points-only layer if (pointsOnlyLayers.indexOf(layer.id) >= 0) { - const features = source.features.data.map(f => f.feature) + + const filtered = new FilteringFeatureSource({ + locationControl: new UIEventSource(undefined), + allElements: undefined, + selectedElement: new UIEventSource(undefined) + }, + Tiles.tile_index(0,0,0), + source, + new UIEventSource(undefined) + ) + const features = filtered.features.data.map(f => f.feature) + const points = features.map(feature => GeoOperations.centerpoint(feature)) console.log("Writing points overview for ", layerId) const targetPath = targetdir + "_" + layerId + "_points.geojson" @@ -325,7 +354,7 @@ async function main(args: string[]) { console.log("Cache builder started with args ", args.join(", ")) if (args.length < 6) { - console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...]\n" + + console.error("Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" + "Note: a new directory named will be created in targetdirectory") return; } @@ -343,10 +372,7 @@ async function main(args: string[]) { const lat1 = Number(args[5]) const lon1 = Number(args[6]) - let generatePointLayersFor = [] - if (args[7] == "--generate-point-overview") { - generatePointLayersFor = args[8].split(",") - } + const tileRange = Tiles.TileRangeBetween(zoomlevel, lat0, lon0, lat1, lon1) @@ -365,6 +391,32 @@ async function main(args: string[]) { console.error("The theme " + theme + " was not found; try one of ", keys); return } + + let generatePointLayersFor = [] + if (args[7] == "--generate-point-overview") { + if(args[8] === undefined){ + throw "--generate-point-overview needs a list of layers to generate the overview for (or * for all)" + }else if (args[8] === '*'){ + generatePointLayersFor = theme.layers.map(l => l.id) + }else{ + generatePointLayersFor = args[8].split(",") + } + console.log("Also generating a point overview for layers ", generatePointLayersFor.join(",")) + } + { + + const index = args.indexOf("--force-zoom-level") + if(index >= 0){ + const forcedZoomLevel = Number(args[index + 1]) + for (const layer of theme.layers) { + layer.source.geojsonSource = "https://127.0.0.1/cache_{layer}_{z}_{x}_{y}.geojson" + layer.source.isOsmCacheLayer = true + layer.source.geojsonZoomLevel = forcedZoomLevel + } + } + } + + const relationTracker = new RelationsTracker() let failed = 0;