diff --git a/scripts/osm2pgsql/generateBuildDbScript.ts b/scripts/osm2pgsql/generateBuildDbScript.ts index 4559f46831..5f6eb28b7f 100644 --- a/scripts/osm2pgsql/generateBuildDbScript.ts +++ b/scripts/osm2pgsql/generateBuildDbScript.ts @@ -7,6 +7,7 @@ import { Or } from "../../src/Logic/Tags/Or" import { RegexTag } from "../../src/Logic/Tags/RegexTag" import { ValidateThemeEnsemble } from "../../src/Models/ThemeConfig/Conversion/Validation" import { AllKnownLayouts } from "../../src/Customizations/AllKnownLayouts" +import { OsmObject } from "../../src/Logic/Osm/OsmObject" class LuaSnippets { @@ -20,6 +21,33 @@ class LuaSnippets { "end", ].join("\n") + public static isPolygonFeature(): { blacklist: TagsFilter, whitelisted: TagsFilter } { + const dict = OsmObject.polygonFeatures + const or: TagsFilter[] = [] + const blacklisted : TagsFilter[] = [] + dict.forEach(({ values, blacklist }, k) => { + if(blacklist){ + if(values === undefined){ + blacklisted.push(new RegexTag(k, /.+/is)) + return + } + values.forEach(v => { + blacklisted.push(new RegexTag(k, v)) + }) + return + } + if (values === undefined || values === null) { + or.push(new RegexTag(k, /.+/is)) + return + } + values.forEach(v => { + or.push(new RegexTag(k, v)) + }) + }) + console.log("Polygon features are:", or.map(t => t.asHumanString(false, false, {}))) + return { blacklist: new Or(blacklisted), whitelisted: new Or(or) } + } + public static toLuaFilter(tag: TagsFilter, useParens: boolean = false): string { if (tag instanceof Tag) { return `object.tags["${tag.key}"] == "${tag.value}"` @@ -55,6 +83,10 @@ class LuaSnippets { return `object.tags["${tag.key}"] ~= "${tag.value}"` } + if (typeof tag.value === "string" && !tag.invert) { + return `object.tags["${tag.key}"] == "${tag.value}"` + } + const v = (tag.value).source.replace(/\\\//g, "/") if ("" + tag.value === "/.+/is" && !tag.invert) { @@ -220,6 +252,7 @@ class GenerateBuildDbScript extends Script { bodyPolygons.push(this.insertInto(tags, layerId, "polygons_").join("\n")) }) + const isPolygon = LuaSnippets.isPolygonFeature() return [ "function process_polygon(object, geom)", " local matches_filter", @@ -232,7 +265,9 @@ class GenerateBuildDbScript extends Script { "", "function osm2pgsql.process_way(object)", this.earlyAbort(), - " if object.is_closed then", + " local object_is_line = not object.is_closed or "+LuaSnippets.toLuaFilter(isPolygon.blacklist), + ` local object_is_area = object.is_closed and (object.tags["area"] == "yes" or (not object_is_line and ${LuaSnippets.toLuaFilter(isPolygon.whitelisted, true)}))`, + " if object_is_area then", " process_polygon(object, object:as_polygon())", " else", " process_linestring(object, object:as_linestring())", diff --git a/src/Logic/FeatureSource/Sources/LayoutSource.ts b/src/Logic/FeatureSource/Sources/LayoutSource.ts index a46d813842..9361217cef 100644 --- a/src/Logic/FeatureSource/Sources/LayoutSource.ts +++ b/src/Logic/FeatureSource/Sources/LayoutSource.ts @@ -59,7 +59,7 @@ export default class LayoutSource extends FeatureSourceMerger { zoom, featureSwitches )//*/ - +/* const osmApiSource = LayoutSource.setupOsmApiSource( osmLayers, bounds, @@ -67,14 +67,14 @@ export default class LayoutSource extends FeatureSourceMerger { backend, featureSwitches, fullNodeDatabaseSource - ) + )*/ const geojsonSources: FeatureSource[] = geojsonlayers.map((l) => LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id)) ) - super(osmApiSource, ...geojsonSources, ...fromCache, ...mvtSources) + super(...geojsonSources, ...fromCache, ...mvtSources) const self = this function setIsLoading() { @@ -83,7 +83,7 @@ export default class LayoutSource extends FeatureSourceMerger { } // overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading()) - osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading()) + // osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading()) } private static setupMvtSource(layer: LayerConfig, mapProperties: { zoom: Store; bounds: Store }, isActive?: Store): FeatureSource{ diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts index 3b7476768f..ed379bfb1a 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource.ts @@ -1,5 +1,5 @@ import { Store } from "../../UIEventSource" -import DynamicTileSource, { PolygonSourceMerger } from "./DynamicTileSource" +import DynamicTileSource from "./DynamicTileSource" import { Utils } from "../../../Utils" import { BBox } from "../../BBox" import LayerConfig from "../../../Models/ThemeConfig/LayerConfig" @@ -7,6 +7,8 @@ import MvtSource from "../Sources/MvtSource" import { Tiles } from "../../../Models/TileRange" import Constants from "../../../Models/Constants" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" +import { LineSourceMerger } from "./LineSourceMerger" +import { PolygonSourceMerger } from "./PolygonSourceMerger" class PolygonMvtSource extends PolygonSourceMerger{ @@ -39,6 +41,36 @@ class PolygonMvtSource extends PolygonSourceMerger{ } +class LineMvtSource extends LineSourceMerger{ + constructor( layer: LayerConfig, + mapProperties: { + zoom: Store + bounds: Store + }, + options?: { + isActive?: Store + }) { + const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14)) + super( + roundedZoom, + layer.minzoom, + (zxy) => { + const [z, x, y] = Tiles.tile_from_index(zxy) + const url = Utils.SubstituteKeys(Constants.VectorTileServer, + { + z, x, y, layer: layer.id, + type: "lines", + }) + return new MvtSource(url, x, y, z) + }, + mapProperties, + { + isActive: options?.isActive, + }) + } +} + + class PointMvtSource extends DynamicTileSource { constructor( @@ -84,9 +116,9 @@ export default class DynamicMvtileSource extends FeatureSourceMerger { isActive?: Store }, ) { - const roundedZoom = mapProperties.zoom.mapD(z => Math.floor(z)) super( new PointMvtSource(layer, mapProperties, options), + new LineMvtSource(layer, mapProperties, options), new PolygonMvtSource(layer, mapProperties, options) ) diff --git a/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts b/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts index f905ae893f..efd109fd5f 100644 --- a/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts +++ b/src/Logic/FeatureSource/TiledFeatureSource/DynamicTileSource.ts @@ -1,11 +1,8 @@ import { Store, Stores } from "../../UIEventSource" import { Tiles } from "../../../Models/TileRange" import { BBox } from "../../BBox" -import { FeatureSource, FeatureSourceForTile } from "../FeatureSource" +import { FeatureSource } from "../FeatureSource" import FeatureSourceMerger from "../Sources/FeatureSourceMerger" -import { Feature } from "geojson" -import { Utils } from "../../../Utils" -import { GeoOperations } from "../../GeoOperations" /*** @@ -84,68 +81,3 @@ export default class DynamicTileSource { - constructor( - zoomlevel: Store, - minzoom: number, - constructSource: (tileIndex: number) => FeatureSourceForTile, - mapProperties: { - bounds: Store - zoom: Store - }, - options?: { - isActive?: Store - }, - ) { - super(zoomlevel, minzoom, constructSource, mapProperties, options) - } - - protected addDataFromSources(sources: FeatureSourceForTile[]) { - sources = Utils.NoNull(sources) - const all: Map = new Map() - const zooms: Map = new Map() - - for (const source of sources) { - let z = source.z - for (const f of source.features.data) { - const id = f.properties.id - if(id.endsWith("146616907")){ - console.log("Horeca totaal") - } - if (!all.has(id)) { - // No other parts of this polygon have been seen before, simply add it - all.set(id, f) - zooms.set(id, z) - continue - } - - // A part of this object has been seen before, eventually from a different zoom level - const oldV = all.get(id) - const oldZ = zooms.get(id) - if (oldZ > z) { - // The store contains more detailed information, so we ignore this part which has a lower accuraccy - continue - } - if (oldZ < z) { - // The old value has worse accuracy then what we receive now, we throw it away - all.set(id, f) - zooms.set(id, z) - continue - } - const merged = GeoOperations.union(f, oldV) - merged.properties = oldV.properties - all.set(id, merged) - zooms.set(id, z) - } - } - - const newList = Array.from(all.values()) - this.features.setData(newList) - this._featuresById.setData(all) - } - -} diff --git a/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts b/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts new file mode 100644 index 0000000000..8a1cb39098 --- /dev/null +++ b/src/Logic/FeatureSource/TiledFeatureSource/LineSourceMerger.ts @@ -0,0 +1,80 @@ +import { FeatureSourceForTile } from "../FeatureSource" +import { Store } from "../../UIEventSource" +import { BBox } from "../../BBox" +import { Utils } from "../../../Utils" +import { Feature, LineString, MultiLineString, Position } from "geojson" +import { Tiles } from "../../../Models/TileRange" +import { GeoOperations } from "../../GeoOperations" +import DynamicTileSource from "./DynamicTileSource" + +/** + * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. + * This is used to reconstruct polygons of vector tiles + */ +export class LineSourceMerger extends DynamicTileSource { + private readonly _zoomlevel: Store + + constructor( + zoomlevel: Store, + minzoom: number, + constructSource: (tileIndex: number) => FeatureSourceForTile, + mapProperties: { + bounds: Store + zoom: Store + }, + options?: { + isActive?: Store + }, + ) { + super(zoomlevel, minzoom, constructSource, mapProperties, options) + this._zoomlevel = zoomlevel + } + + protected addDataFromSources(sources: FeatureSourceForTile[]) { + sources = Utils.NoNull(sources) + const all: Map> = new Map() + const currentZoom = this._zoomlevel?.data ?? 0 + for (const source of sources) { + if(source.z != currentZoom){ + continue + } + const bboxCoors = Tiles.tile_bounds_lon_lat(source.z, source.x, source.y) + const bboxGeo = new BBox(bboxCoors).asGeoJson({}) + for (const f of source.features.data) { + const id = f.properties.id + const coordinates : Position[][] = [] + if(f.geometry.type === "LineString"){ + coordinates.push(f.geometry.coordinates) + }else if(f.geometry.type === "MultiLineString"){ + coordinates.push(...f.geometry.coordinates) + }else { + console.error("Invalid geometry type:", f.geometry.type) + continue + } + const oldV = all.get(id) + if(!oldV){ + + all.set(id, { + type: "Feature", + properties: f.properties, + geometry:{ + type:"MultiLineString", + coordinates + } + }) + continue + } + oldV.geometry.coordinates.push(...coordinates) + } + } + + const keys = Array.from(all.keys()) + for (const key of keys) { + all.set(key, GeoOperations.attemptLinearize(>all.get(key))) + } + const newList = Array.from(all.values()) + this.features.setData(newList) + this._featuresById.setData(all) + } + +} diff --git a/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts b/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts new file mode 100644 index 0000000000..47327d8726 --- /dev/null +++ b/src/Logic/FeatureSource/TiledFeatureSource/PolygonSourceMerger.ts @@ -0,0 +1,73 @@ +import { FeatureSourceForTile } from "../FeatureSource" +import { Store } from "../../UIEventSource" +import { BBox } from "../../BBox" +import { Utils } from "../../../Utils" +import { Feature } from "geojson" +import { GeoOperations } from "../../GeoOperations" +import DynamicTileSource from "./DynamicTileSource" + +/** + * The PolygonSourceMerger receives various small pieces of bigger polygons and stitches them together. + * This is used to reconstruct polygons of vector tiles + */ +export class PolygonSourceMerger extends DynamicTileSource { + constructor( + zoomlevel: Store, + minzoom: number, + constructSource: (tileIndex: number) => FeatureSourceForTile, + mapProperties: { + bounds: Store + zoom: Store + }, + options?: { + isActive?: Store + }, + ) { + super(zoomlevel, minzoom, constructSource, mapProperties, options) + } + + protected addDataFromSources(sources: FeatureSourceForTile[]) { + sources = Utils.NoNull(sources) + const all: Map = new Map() + const zooms: Map = new Map() + + for (const source of sources) { + let z = source.z + for (const f of source.features.data) { + const id = f.properties.id + if (id.endsWith("146616907")) { + console.log("Horeca totaal") + } + if (!all.has(id)) { + // No other parts of this polygon have been seen before, simply add it + all.set(id, f) + zooms.set(id, z) + continue + } + + // A part of this object has been seen before, eventually from a different zoom level + const oldV = all.get(id) + const oldZ = zooms.get(id) + if (oldZ > z) { + // The store contains more detailed information, so we ignore this part which has a lower accuraccy + continue + } + if (oldZ < z) { + // The old value has worse accuracy then what we receive now, we throw it away + all.set(id, f) + zooms.set(id, z) + continue + } + const merged = GeoOperations.union(f, oldV) + merged.properties = oldV.properties + all.set(id, merged) + zooms.set(id, z) + } + } + + const newList = Array.from(all.values()) + this.features.setData(newList) + this._featuresById.setData(all) + } + +} diff --git a/src/Logic/GeoOperations.ts b/src/Logic/GeoOperations.ts index c90869b839..7567a8bca8 100644 --- a/src/Logic/GeoOperations.ts +++ b/src/Logic/GeoOperations.ts @@ -1,6 +1,6 @@ import { BBox } from "./BBox" import * as turf from "@turf/turf" -import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf" +import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf" import { Feature, FeatureCollection, @@ -156,7 +156,7 @@ export class GeoOperations { const intersection = GeoOperations.calculateIntersection( feature, otherFeature, - featureBBox + featureBBox, ) if (intersection === null) { continue @@ -195,7 +195,7 @@ export class GeoOperations { console.error( "Could not correctly calculate the overlap of ", feature, - ": unsupported type" + ": unsupported type", ) return result } @@ -224,7 +224,7 @@ export class GeoOperations { */ public static inside( pointCoordinate: [number, number] | Feature, - feature: Feature + feature: Feature, ): boolean { // ray-casting algorithm based on // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html @@ -302,7 +302,7 @@ export class GeoOperations { */ public static nearestPoint( way: Feature, - point: [number, number] + point: [number, number], ): Feature< Point, { @@ -324,11 +324,11 @@ export class GeoOperations { public static forceLineString(way: Feature): Feature public static forceLineString( - way: Feature + way: Feature, ): Feature public static forceLineString( - way: Feature + way: Feature, ): Feature { if (way.geometry.type === "Polygon") { way = { ...way } @@ -448,7 +448,7 @@ export class GeoOperations { */ public static LineIntersections( feature: Feature, - otherFeature: Feature + otherFeature: Feature, ): [number, number][] { return turf .lineIntersect(feature, otherFeature) @@ -485,7 +485,7 @@ export class GeoOperations { locations: | Feature | Feature[], - title?: string + title?: string, ) { title = title?.trim() if (title === undefined || title === "") { @@ -506,7 +506,7 @@ export class GeoOperations { type: "Point", coordinates: p, }, - } + }, ) } for (const l of locationsWithMeta) { @@ -521,7 +521,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -539,7 +539,7 @@ export class GeoOperations { */ public static toGpxPoints( locations: Feature[], - title?: string + title?: string, ) { title = title?.trim() if (title === undefined || title === "") { @@ -560,7 +560,7 @@ export class GeoOperations { trackPoints.push(trkpt) } const header = - '' + "" return ( header + "\n" + @@ -648,7 +648,7 @@ export class GeoOperations { }, }, distanceMeter, - { units: "meters" } + { units: "meters" }, ).geometry.coordinates } @@ -683,7 +683,7 @@ export class GeoOperations { */ static completelyWithin( feature: Feature, - possiblyEnclosingFeature: Feature + possiblyEnclosingFeature: Feature, ): boolean { return booleanWithin(feature, possiblyEnclosingFeature) } @@ -714,6 +714,23 @@ export class GeoOperations { } return kept } + + if (toSplit.geometry.type === "MultiLineString") { + const lines: Feature[][] = toSplit.geometry.coordinates.map(coordinates => + turf.lineSplit( {type: "LineString", coordinates}, boundary).features ) + const splitted: Feature[] = [].concat(...lines) + const kept: Feature[] = [] + for (const f of splitted) { + console.log("Checking", f) + if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) { + continue + } + f.properties = { ...toSplit.properties } + kept.push(f) + } + console.log(">>>", {lines, splitted, kept}) + return kept + } if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") { const splitup = turf.intersect(>toSplit, boundary) splitup.properties = { ...toSplit.properties } @@ -739,7 +756,7 @@ export class GeoOperations { */ public static featureToCoordinateWithRenderingType( feature: Feature, - location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string + location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string, ): [number, number] | undefined { switch (location) { case "point": @@ -760,7 +777,7 @@ export class GeoOperations { const centerpoint = GeoOperations.centerpointCoordinates(feature) const projected = GeoOperations.nearestPoint( >feature, - centerpoint + centerpoint, ) return <[number, number]>projected.geometry.coordinates } @@ -937,7 +954,7 @@ export class GeoOperations { * GeoOperations.bearingToHuman(46) // => "NE" */ public static bearingToHuman( - bearing: number + bearing: number, ): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" { while (bearing < 0) { bearing += 360 @@ -956,7 +973,7 @@ export class GeoOperations { * GeoOperations.bearingToHuman(46) // => "NE" */ public static bearingToHumanRelative( - bearing: number + bearing: number, ): | "straight" | "slight_right" @@ -975,18 +992,73 @@ export class GeoOperations { return GeoOperations.directionsRelative[segment] } + /** + * const coors = [[[3.217198532946432,51.218067],[3.216807134449482,51.21849812105347],[3.2164304037883706,51.2189272]],[[3.2176208,51.21760169669458],[3.217198560167068,51.218067]]] + * const f = {geometry: {coordinates: coors}} + * const merged = GeoOperations.attemptLinearize(f) + * merged.geometry.coordinates // => [[3.2176208,51.21760169669458],[3.217198532946432,51.218067], [3.216807134449482,51.21849812105347],[3.2164304037883706,51.2189272]] + */ + static attemptLinearize(multiLineStringFeature: Feature): Feature { + const coors = multiLineStringFeature.geometry.coordinates + if(coors.length === 0) { + console.error(multiLineStringFeature.geometry) + throw "Error: got degenerate multilinestring" + } + outer: for (let i = coors.length - 1; i >= 0; i--) { + // We try to match the first element of 'i' with another, earlier list `j` + // If a match is found with `j`, j is extended and `i` is scrapped + const iFirst = coors[i][0] + for (let j = 0; j < coors.length; j++) { + if (i == j) { + continue + } + + const jLast = coors[j].at(-1) + if (!(Math.abs(iFirst[0] - jLast[0]) < 0.000001 && Math.abs(iFirst[1] - jLast[1]) < 0.0000001)) { + continue + } + coors[j].splice(coors.length - 1, 1) + coors[j].push(...coors[i]) + coors.splice(i, 1) + continue outer + } + } + if(coors.length === 0) { + throw "No more coordinates found" + } + + if (coors.length === 1) { + return { + type: "Feature", + properties: multiLineStringFeature.properties, + geometry: { + type: "LineString", + coordinates: coors[0], + }, + } + } + return { + type: "Feature", + properties: multiLineStringFeature.properties, + geometry: { + type: "MultiLineString", + coordinates: coors, + }, + } + } + /** * Helper function which does the heavy lifting for 'inside' */ private static pointInPolygonCoordinates( x: number, y: number, - coordinates: [number, number][][] + coordinates: [number, number][][], ): boolean { const inside = GeoOperations.pointWithinRing( x, y, - /*This is the outer ring of the polygon */ coordinates[0] + /*This is the outer ring of the polygon */ coordinates[0], ) if (!inside) { return false @@ -995,7 +1067,7 @@ export class GeoOperations { const inHole = GeoOperations.pointWithinRing( x, y, - coordinates[i] /* These are inner rings, aka holes*/ + coordinates[i], /* These are inner rings, aka holes*/ ) if (inHole) { return false @@ -1033,7 +1105,7 @@ export class GeoOperations { feature, otherFeature, featureBBox: BBox, - otherFeatureBBox?: BBox + otherFeatureBBox?: BBox, ): number { if (feature.geometry.type === "LineString") { otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature) @@ -1082,7 +1154,7 @@ export class GeoOperations { let intersection = turf.lineSlice( turf.point(intersectionPointsArray[0]), turf.point(intersectionPointsArray[1]), - feature + feature, ) if (intersection == null) { @@ -1103,7 +1175,7 @@ export class GeoOperations { otherFeature, feature, otherFeatureBBox, - featureBBox + featureBBox, ) } @@ -1123,7 +1195,7 @@ export class GeoOperations { console.log("Applying fallback intersection...") const intersection = turf.intersect( turf.truncate(feature), - turf.truncate(otherFeature) + turf.truncate(otherFeature), ) if (intersection == null) { return null diff --git a/src/Logic/Osm/OsmObject.ts b/src/Logic/Osm/OsmObject.ts index f0e0215cde..d743b58c96 100644 --- a/src/Logic/Osm/OsmObject.ts +++ b/src/Logic/Osm/OsmObject.ts @@ -7,7 +7,7 @@ import { Feature, LineString, Polygon } from "geojson" export abstract class OsmObject { private static defaultBackend = "https://api.openstreetmap.org/" protected static backendURL = OsmObject.defaultBackend - private static polygonFeatures = OsmObject.constructPolygonFeatures() + public static polygonFeatures = OsmObject.constructPolygonFeatures() type: "node" | "way" | "relation" id: number /**