forked from MapComplete/MapComplete
Add polygon merging
This commit is contained in:
parent
ee38cdb9d7
commit
ee3e000cd1
11 changed files with 460 additions and 305 deletions
|
@ -29,7 +29,7 @@ Install osm2pgsql (hint: compile from source is painless)
|
|||
To seed the database:
|
||||
|
||||
````
|
||||
osm2pgsql -O flex -E 4326 -S build_db.lua -s --flat-nodes=import-help-file -d postgresql://user:password@localhost:5444/osm-poi <file>.osm.pbf
|
||||
osm2pgsql -O flex -S build_db.lua -s --flat-nodes=import-help-file -d postgresql://user:password@localhost:5444/osm-poi <file>.osm.pbf
|
||||
````
|
||||
Storing properties to table '"public"."osm2pgsql_properties" takes about 25 minutes with planet.osm
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
"oauth_secret": "NBWGhWDrD3QDB35xtVuxv4aExnmIt4FA_WgeLtwxasg",
|
||||
"url": "https://www.openstreetmap.org"
|
||||
},
|
||||
"mvt_layer_server": "http://127.0.0.1:7800/public.{layer}/{z}/{x}/{y}.pbf",
|
||||
"mvt_layer_server": "http://127.0.0.1:7800/public.{type}_{layer}/{z}/{x}/{y}.pbf",
|
||||
"disabled:oauth_credentials": {
|
||||
"##": "DEV",
|
||||
"#": "This client-id is registered by 'MapComplete' on https://master.apis.dev.openstreetmap.org/",
|
||||
|
|
|
@ -5,91 +5,57 @@ import Script from "../Script"
|
|||
import fs from "fs"
|
||||
import { Or } from "../../src/Logic/Tags/Or"
|
||||
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
|
||||
import { Utils } from "../../src/Utils"
|
||||
import { ValidateThemeEnsemble } from "../../src/Models/ThemeConfig/Conversion/Validation"
|
||||
import { AllKnownLayouts } from "../../src/Customizations/AllKnownLayouts"
|
||||
|
||||
class LuaSnippets {
|
||||
/**
|
||||
* The main piece of code that calls `process_poi`
|
||||
*/
|
||||
static tail = [
|
||||
"function osm2pgsql.process_node(object)",
|
||||
" process_poi(object, object:as_point())",
|
||||
|
||||
public static helpers = [
|
||||
"function countTbl(tbl)\n" +
|
||||
" local c = 0\n" +
|
||||
" for n in pairs(tbl) do \n" +
|
||||
" c = c + 1 \n" +
|
||||
" end\n" +
|
||||
" return c\n" +
|
||||
"end",
|
||||
"",
|
||||
"function osm2pgsql.process_way(object)",
|
||||
" if object.is_closed then",
|
||||
" process_poi(object, object:as_polygon():centroid())",
|
||||
" end",
|
||||
"end",
|
||||
""].join("\n")
|
||||
].join("\n")
|
||||
|
||||
public static combine(calls: string[]): string {
|
||||
return [
|
||||
`function process_poi(object, geom)`,
|
||||
...calls.map(c => " " + c + "(object, geom)"),
|
||||
`end`,
|
||||
].join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
class GenerateLayerLua {
|
||||
private readonly _id: string
|
||||
private readonly _tags: TagsFilter
|
||||
private readonly _foundInThemes: string[]
|
||||
|
||||
constructor(id: string, tags: TagsFilter, foundInThemes: string[] = []) {
|
||||
this._tags = tags
|
||||
this._id = id
|
||||
this._foundInThemes = foundInThemes
|
||||
}
|
||||
|
||||
public functionName() {
|
||||
if (!this._tags) {
|
||||
return undefined
|
||||
public static toLuaFilter(tag: TagsFilter, useParens: boolean = false): string {
|
||||
if (tag instanceof Tag) {
|
||||
return `object.tags["${tag.key}"] == "${tag.value}"`
|
||||
}
|
||||
return `process_poi_${this._id}`
|
||||
}
|
||||
|
||||
public generateFunction(): string {
|
||||
if (!this._tags) {
|
||||
return undefined
|
||||
if (tag instanceof And) {
|
||||
const expr = tag.and.map(t => this.toLuaFilter(t, true)).join(" and ")
|
||||
if (useParens) {
|
||||
return "(" + expr + ")"
|
||||
}
|
||||
return expr
|
||||
}
|
||||
return [
|
||||
`local pois_${this._id} = osm2pgsql.define_table({`,
|
||||
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
|
||||
` name = '${this._id}',`,
|
||||
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
|
||||
" columns = {",
|
||||
" { column = 'tags', type = 'jsonb' },",
|
||||
" { column = 'geom', type = 'point', projection = 4326, not_null = true },",
|
||||
" }" +
|
||||
"})",
|
||||
"",
|
||||
"",
|
||||
`function ${this.functionName()}(object, geom)`,
|
||||
" local matches_filter = " + this.toLuaFilter(this._tags),
|
||||
" if( not matches_filter) then",
|
||||
" return",
|
||||
" end",
|
||||
" local a = {",
|
||||
" geom = geom,",
|
||||
" tags = object.tags",
|
||||
" }",
|
||||
" ",
|
||||
` pois_${this._id}:insert(a)`,
|
||||
"end",
|
||||
"",
|
||||
].join("\n")
|
||||
if (tag instanceof Or) {
|
||||
const expr = tag.or.map(t => this.toLuaFilter(t, true)).join(" or ")
|
||||
if (useParens) {
|
||||
return "(" + expr + ")"
|
||||
}
|
||||
return expr
|
||||
}
|
||||
if (tag instanceof RegexTag) {
|
||||
let expr = LuaSnippets.regexTagToLua(tag)
|
||||
if (useParens) {
|
||||
expr = "(" + expr + ")"
|
||||
}
|
||||
return expr
|
||||
}
|
||||
let msg = "Could not handle" + tag.asHumanString(false, false, {})
|
||||
console.error(msg)
|
||||
throw msg
|
||||
}
|
||||
|
||||
private regexTagToLua(tag: RegexTag) {
|
||||
private static regexTagToLua(tag: RegexTag) {
|
||||
if (typeof tag.value === "string" && tag.invert) {
|
||||
return `object.tags["${tag.key}"] ~= "${tag.value}"`
|
||||
}
|
||||
|
||||
const v = (<RegExp> tag.value).source.replace(/\\\//g, "/")
|
||||
const v = (<RegExp>tag.value).source.replace(/\\\//g, "/")
|
||||
|
||||
if ("" + tag.value === "/.+/is" && !tag.invert) {
|
||||
return `object.tags["${tag.key}"] ~= nil`
|
||||
|
@ -115,35 +81,58 @@ class GenerateLayerLua {
|
|||
return `(object.tags["${tag.key}"] ~= nil and string.find(object.tags["${tag.key}"], "${v}"))`
|
||||
}
|
||||
|
||||
private toLuaFilter(tag: TagsFilter, useParens: boolean = false): string {
|
||||
if (tag instanceof Tag) {
|
||||
return `object.tags["${tag.key}"] == "${tag.value}"`
|
||||
}
|
||||
if (tag instanceof And) {
|
||||
const expr = tag.and.map(t => this.toLuaFilter(t, true)).join(" and ")
|
||||
if (useParens) {
|
||||
return "(" + expr + ")"
|
||||
}
|
||||
return expr
|
||||
}
|
||||
if (tag instanceof Or) {
|
||||
const expr = tag.or.map(t => this.toLuaFilter(t, true)).join(" or ")
|
||||
if (useParens) {
|
||||
return "(" + expr + ")"
|
||||
}
|
||||
return expr
|
||||
}
|
||||
if (tag instanceof RegexTag) {
|
||||
let expr = this.regexTagToLua(tag)
|
||||
if (useParens) {
|
||||
expr = "(" + expr + ")"
|
||||
}
|
||||
return expr
|
||||
}
|
||||
let msg = "Could not handle" + tag.asHumanString(false, false, {})
|
||||
console.error(msg)
|
||||
throw msg
|
||||
}
|
||||
|
||||
class GenerateLayerLua {
|
||||
private readonly _id: string
|
||||
private readonly _tags: TagsFilter
|
||||
private readonly _foundInThemes: string[]
|
||||
|
||||
constructor(id: string, tags: TagsFilter, foundInThemes: string[] = []) {
|
||||
this._tags = tags
|
||||
this._id = id
|
||||
this._foundInThemes = foundInThemes
|
||||
}
|
||||
|
||||
public generateTables(): string {
|
||||
if (!this._tags) {
|
||||
return undefined
|
||||
}
|
||||
return [
|
||||
`db_tables.pois_${this._id} = osm2pgsql.define_table({`,
|
||||
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
|
||||
` name = 'pois_${this._id}',`,
|
||||
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
|
||||
" columns = {",
|
||||
" { column = 'tags', type = 'jsonb' },",
|
||||
" { column = 'geom', type = 'point', projection = 4326, not_null = true },",
|
||||
" }",
|
||||
"})",
|
||||
"",
|
||||
`db_tables.lines_${this._id} = osm2pgsql.define_table({`,
|
||||
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
|
||||
` name = 'lines_${this._id}',`,
|
||||
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
|
||||
" columns = {",
|
||||
" { column = 'tags', type = 'jsonb' },",
|
||||
" { column = 'geom', type = 'linestring', projection = 4326, not_null = true },",
|
||||
" }",
|
||||
"})",
|
||||
|
||||
`db_tables.polygons_${this._id} = osm2pgsql.define_table({`,
|
||||
this._foundInThemes ? "-- used in themes: " + this._foundInThemes.join(", ") : "",
|
||||
` name = 'polygons_${this._id}',`,
|
||||
" ids = { type = 'any', type_column = 'osm_type', id_column = 'osm_id' },",
|
||||
" columns = {",
|
||||
" { column = 'tags', type = 'jsonb' },",
|
||||
" { column = 'geom', type = 'polygon', projection = 4326, not_null = true },",
|
||||
" }",
|
||||
"})",
|
||||
"",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class GenerateBuildDbScript extends Script {
|
||||
|
@ -163,14 +152,93 @@ class GenerateBuildDbScript extends Script {
|
|||
})
|
||||
|
||||
const script = [
|
||||
...generators.map(g => g.generateFunction()),
|
||||
LuaSnippets.combine(Utils.NoNull(generators.map(g => g.functionName()))),
|
||||
LuaSnippets.tail,
|
||||
"local db_tables = {}",
|
||||
LuaSnippets.helpers,
|
||||
...generators.map(g => g.generateTables()),
|
||||
this.generateProcessPoi(allNeededLayers),
|
||||
this.generateProcessWay(allNeededLayers),
|
||||
].join("\n\n\n")
|
||||
const path = "build_db.lua"
|
||||
fs.writeFileSync(path, script, "utf-8")
|
||||
console.log("Written", path)
|
||||
console.log(allNeededLayers.size+" layers will be created. Make sure to set 'max_connections' to at least "+(10 + allNeededLayers.size) )
|
||||
console.log(allNeededLayers.size + " layers will be created with 3 tables each. Make sure to set 'max_connections' to at least " + (10 + 3 * allNeededLayers.size))
|
||||
}
|
||||
|
||||
private earlyAbort() {
|
||||
return [" if countTbl(object.tags) == 0 then",
|
||||
" return",
|
||||
" end",
|
||||
""].join("\n")
|
||||
}
|
||||
|
||||
private generateProcessPoi(allNeededLayers: Map<string, { tags: TagsFilter; foundInTheme: string[] }>) {
|
||||
const body: string[] = []
|
||||
allNeededLayers.forEach(({ tags }, layerId) => {
|
||||
body.push(
|
||||
this.insertInto(tags, layerId, "pois_").join("\n"),
|
||||
)
|
||||
})
|
||||
|
||||
return [
|
||||
"function osm2pgsql.process_node(object)",
|
||||
this.earlyAbort(),
|
||||
" local geom = object:as_point()",
|
||||
" local matches_filter = false",
|
||||
body.join("\n"),
|
||||
"end",
|
||||
].join("\n")
|
||||
}
|
||||
|
||||
/**
|
||||
* If matches_filter
|
||||
* @param tags
|
||||
* @param layerId
|
||||
* @param tableprefix
|
||||
* @private
|
||||
*/
|
||||
private insertInto(tags: TagsFilter, layerId: string, tableprefix: "pois_" | "lines_" | "polygons_") {
|
||||
const filter = LuaSnippets.toLuaFilter(tags)
|
||||
return [
|
||||
" matches_filter = " + filter,
|
||||
" if matches_filter then",
|
||||
" db_tables." + tableprefix + layerId + ":insert({",
|
||||
" geom = geom,",
|
||||
" tags = object.tags",
|
||||
" })",
|
||||
" end",
|
||||
]
|
||||
}
|
||||
|
||||
private generateProcessWay(allNeededLayers: Map<string, { tags: TagsFilter }>) {
|
||||
const bodyLines: string[] = []
|
||||
allNeededLayers.forEach(({ tags }, layerId) => {
|
||||
bodyLines.push(this.insertInto(tags, layerId, "lines_").join("\n"))
|
||||
})
|
||||
|
||||
const bodyPolygons: string[] = []
|
||||
allNeededLayers.forEach(({ tags }, layerId) => {
|
||||
bodyPolygons.push(this.insertInto(tags, layerId, "polygons_").join("\n"))
|
||||
})
|
||||
|
||||
return [
|
||||
"function process_polygon(object, geom)",
|
||||
" local matches_filter",
|
||||
...bodyPolygons,
|
||||
"end",
|
||||
"function process_linestring(object, geom)",
|
||||
" local matches_filter",
|
||||
...bodyLines,
|
||||
"end",
|
||||
"",
|
||||
"function osm2pgsql.process_way(object)",
|
||||
this.earlyAbort(),
|
||||
" if object.is_closed then",
|
||||
" process_polygon(object, object:as_polygon())",
|
||||
" else",
|
||||
" process_linestring(object, object:as_linestring())",
|
||||
" end",
|
||||
"end",
|
||||
].join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,12 @@ export interface FeatureSourceForLayer<T extends Feature = Feature> extends Feat
|
|||
readonly layer: FilteredLayer
|
||||
}
|
||||
|
||||
export interface FeatureSourceForTile <T extends Feature = Feature> extends FeatureSource<T> {
|
||||
readonly x: number
|
||||
readonly y: number
|
||||
readonly z: number
|
||||
|
||||
}
|
||||
/**
|
||||
* A feature source which is aware of the indexes it contains
|
||||
*/
|
||||
|
|
|
@ -2,43 +2,49 @@ import { Store, UIEventSource } from "../../UIEventSource"
|
|||
import { FeatureSource, IndexedFeatureSource } from "../FeatureSource"
|
||||
import { Feature } from "geojson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import DynamicTileSource from "../TiledFeatureSource/DynamicTileSource"
|
||||
|
||||
/**
|
||||
*
|
||||
* The featureSourceMerger receives complete geometries from various sources.
|
||||
* If multiple sources contain the same object (as determined by 'id'), only one copy of them is retained
|
||||
*/
|
||||
export default class FeatureSourceMerger implements IndexedFeatureSource {
|
||||
export default class FeatureSourceMerger<Src extends FeatureSource = FeatureSource> implements IndexedFeatureSource {
|
||||
public features: UIEventSource<Feature[]> = new UIEventSource([])
|
||||
public readonly featuresById: Store<Map<string, Feature>>
|
||||
private readonly _featuresById: UIEventSource<Map<string, Feature>>
|
||||
private readonly _sources: FeatureSource[] = []
|
||||
protected readonly _featuresById: UIEventSource<Map<string, Feature>>
|
||||
private readonly _sources: Src[] = []
|
||||
/**
|
||||
* Merges features from different featureSources.
|
||||
* In case that multiple features have the same id, the latest `_version_number` will be used. Otherwise, we will take the last one
|
||||
*/
|
||||
constructor(...sources: FeatureSource[]) {
|
||||
constructor(...sources: Src[]) {
|
||||
this._featuresById = new UIEventSource<Map<string, Feature>>(new Map<string, Feature>())
|
||||
this.featuresById = this._featuresById
|
||||
const self = this
|
||||
sources = Utils.NoNull(sources)
|
||||
for (let source of sources) {
|
||||
source.features.addCallback(() => {
|
||||
self.addData(sources.map((s) => s.features.data))
|
||||
self.addDataFromSources(sources)
|
||||
})
|
||||
}
|
||||
this.addData(sources.map((s) => s.features.data))
|
||||
this.addDataFromSources(sources)
|
||||
this._sources = sources
|
||||
}
|
||||
|
||||
public addSource(source: FeatureSource) {
|
||||
public addSource(source: Src) {
|
||||
if (!source) {
|
||||
return
|
||||
}
|
||||
this._sources.push(source)
|
||||
source.features.addCallbackAndRun(() => {
|
||||
this.addData(this._sources.map((s) => s.features.data))
|
||||
this.addDataFromSources(this._sources)
|
||||
})
|
||||
}
|
||||
|
||||
protected addDataFromSources(sources: Src[]){
|
||||
this.addData(sources.map(s => s.features.data))
|
||||
}
|
||||
|
||||
protected addData(sources: Feature[][]) {
|
||||
sources = Utils.NoNull(sources)
|
||||
let somethingChanged = false
|
||||
|
@ -56,7 +62,7 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
|
|||
const id = f.properties.id
|
||||
unseen.delete(id)
|
||||
if (!all.has(id)) {
|
||||
// This is a new feature
|
||||
// This is a new, previously unseen feature
|
||||
somethingChanged = true
|
||||
all.set(id, f)
|
||||
continue
|
||||
|
@ -81,10 +87,8 @@ export default class FeatureSourceMerger implements IndexedFeatureSource {
|
|||
return
|
||||
}
|
||||
|
||||
const newList = []
|
||||
all.forEach((value) => {
|
||||
newList.push(value)
|
||||
})
|
||||
const newList = Array.from(all.values())
|
||||
|
||||
this.features.setData(newList)
|
||||
this._featuresById.setData(all)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Feature, Geometry } from "geojson"
|
||||
import { Store, UIEventSource } from "../../UIEventSource"
|
||||
import { FeatureSource } from "../FeatureSource"
|
||||
import { FeatureSourceForTile } from "../FeatureSource"
|
||||
import Pbf from "pbf"
|
||||
import * as pbfCompile from "pbf/compile"
|
||||
import * as PbfSchema from "protocol-buffers-schema"
|
||||
|
@ -19,8 +19,67 @@ class MvtFeatureBuilder {
|
|||
this._y0 = extent * y
|
||||
}
|
||||
|
||||
public toGeoJson(geometry, typeIndex, properties): Feature {
|
||||
let coords: [number, number] | Coords | Coords[] = this.encodeGeometry(geometry)
|
||||
private static signedArea(ring: Coords): number {
|
||||
let sum = 0
|
||||
const len = ring.length
|
||||
// J is basically (i - 1) % len
|
||||
let j = len - 1
|
||||
let p1
|
||||
let p2
|
||||
for (let i = 0; i < len; i++) {
|
||||
p1 = ring[i]
|
||||
p2 = ring[j]
|
||||
sum += (p2.x - p1.x) * (p1.y + p2.y)
|
||||
j = i
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* const rings = [ [ [ 3.208361864089966, 51.186908820014736 ], [ 3.2084155082702637, 51.18689537073311 ], [ 3.208436965942383, 51.186888646090836 ], [ 3.2084155082702637, 51.18686174751187 ], [ 3.2084155082702637, 51.18685502286465 ], [ 3.2083725929260254, 51.18686847215807 ], [ 3.2083404064178467, 51.18687519680333 ], [ 3.208361864089966, 51.186908820014736 ] ] ]
|
||||
* MvtFeatureBuilder.classifyRings(rings) // => [rings]
|
||||
*/
|
||||
private static classifyRings(rings: Coords[]): Coords[][] {
|
||||
if (rings.length <= 0) {
|
||||
throw "Now rings in polygon found"
|
||||
}
|
||||
if (rings.length == 1) {
|
||||
return [rings]
|
||||
}
|
||||
|
||||
const polygons: Coords[][] = []
|
||||
let currentPolygon: Coords[]
|
||||
|
||||
for (let i = 0; i < rings.length; i++) {
|
||||
let ring = rings[i]
|
||||
const area = this.signedArea(ring)
|
||||
if (area === 0) {
|
||||
// Weird, degenerate ring
|
||||
continue
|
||||
}
|
||||
const ccw = area < 0
|
||||
|
||||
if (ccw === (area < 0)) {
|
||||
if (currentPolygon) {
|
||||
polygons.push(currentPolygon)
|
||||
}
|
||||
currentPolygon = [ring]
|
||||
|
||||
} else {
|
||||
currentPolygon.push(ring)
|
||||
}
|
||||
}
|
||||
if (currentPolygon) {
|
||||
polygons.push(currentPolygon)
|
||||
}
|
||||
|
||||
return polygons
|
||||
}
|
||||
|
||||
public toGeoJson(geometry: number[], typeIndex: 1 | 2 | 3, properties: any): Feature {
|
||||
let coords: Coords[] = this.encodeGeometry(geometry)
|
||||
let classified = undefined
|
||||
switch (typeIndex) {
|
||||
case 1:
|
||||
const points = []
|
||||
|
@ -38,9 +97,9 @@ class MvtFeatureBuilder {
|
|||
break
|
||||
|
||||
case 3:
|
||||
let classified = this.classifyRings(coords)
|
||||
for (let i = 0; i < coords.length; i++) {
|
||||
for (let j = 0; j < coords[i].length; j++) {
|
||||
classified = MvtFeatureBuilder.classifyRings(coords)
|
||||
for (let i = 0; i < classified.length; i++) {
|
||||
for (let j = 0; j < classified[i].length; j++) {
|
||||
this.project(classified[i][j])
|
||||
}
|
||||
}
|
||||
|
@ -48,9 +107,11 @@ class MvtFeatureBuilder {
|
|||
}
|
||||
|
||||
let type: string = MvtFeatureBuilder.geom_types[typeIndex]
|
||||
let polygonCoords: Coords | Coords[] | Coords[][]
|
||||
if (coords.length === 1) {
|
||||
coords = coords[0]
|
||||
polygonCoords = (classified ?? coords)[0]
|
||||
} else {
|
||||
polygonCoords = classified ?? coords
|
||||
type = "Multi" + type
|
||||
}
|
||||
|
||||
|
@ -58,13 +119,22 @@ class MvtFeatureBuilder {
|
|||
type: "Feature",
|
||||
geometry: {
|
||||
type: <any>type,
|
||||
coordinates: <any>coords,
|
||||
coordinates: <any>polygonCoords,
|
||||
},
|
||||
properties,
|
||||
}
|
||||
}
|
||||
|
||||
private encodeGeometry(geometry: number[]) {
|
||||
/**
|
||||
*
|
||||
* const geometry = [9,233,8704,130,438,1455,270,653,248,423,368,493,362,381,330,267,408,301,406,221,402,157,1078,429,1002,449,1036,577,800,545,1586,1165,164,79,40]
|
||||
* const builder = new MvtFeatureBuilder(4096, 66705, 43755, 17)
|
||||
* const expected = [[3.2106759399175644,51.213658395282124],[3.2108227908611298,51.21396418776169],[3.2109133154153824,51.21410154168976],[3.210996463894844,51.214190590500664],[3.211119845509529,51.214294340548975],[3.211241215467453,51.2143745681588],[3.2113518565893173,51.21443085341426],[3.211488649249077,51.21449427925393],[3.2116247713565826,51.214540903490956],[3.211759552359581,51.21457408647774],[3.2121209800243378,51.214664394485254],[3.212456926703453,51.21475890267553],[3.2128042727708817,51.214880292910834],[3.213072493672371,51.214994962285544],[3.2136042416095734,51.21523984134939],[3.2136592268943787,51.21525664260963],[3.213672637939453,51.21525664260963]]
|
||||
* builder.project(builder.encodeGeometry(geometry)[0]) // => expected
|
||||
* @param geometry
|
||||
* @private
|
||||
*/
|
||||
private encodeGeometry(geometry: number[]): Coords[] {
|
||||
let cX = 0
|
||||
let cY = 0
|
||||
let coordss: Coords[] = []
|
||||
|
@ -86,7 +156,7 @@ class MvtFeatureBuilder {
|
|||
currentRing = []
|
||||
}
|
||||
}
|
||||
if (commandId === 1 || commandId === 2){
|
||||
if (commandId === 1 || commandId === 2) {
|
||||
for (let j = 0; j < commandCount; j++) {
|
||||
const dx = geometry[i + j * 2 + 1]
|
||||
cX += ((dx >> 1) ^ (-(dx & 1)))
|
||||
|
@ -94,10 +164,11 @@ class MvtFeatureBuilder {
|
|||
cY += ((dy >> 1) ^ (-(dy & 1)))
|
||||
currentRing.push([cX, cY])
|
||||
}
|
||||
i = commandCount * 2
|
||||
i += commandCount * 2
|
||||
}
|
||||
if(commandId === 7){
|
||||
if (commandId === 7) {
|
||||
currentRing.push([...currentRing[0]])
|
||||
i++
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -107,62 +178,12 @@ class MvtFeatureBuilder {
|
|||
return coordss
|
||||
}
|
||||
|
||||
private signedArea(ring: Coords): number {
|
||||
let sum = 0
|
||||
const len = ring.length
|
||||
// J is basically (i - 1) % len
|
||||
let j = len - 1
|
||||
let p1
|
||||
let p2
|
||||
for (let i = 0; i < len; i++) {
|
||||
p1 = ring[i]
|
||||
p2 = ring[j]
|
||||
sum += (p2.x - p1.x) * (p1.y + p2.y)
|
||||
j = i
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
private classifyRings(rings: Coords[]): Coords[][] {
|
||||
const len = rings.length
|
||||
|
||||
if (len <= 1) return [rings]
|
||||
|
||||
const polygons = []
|
||||
let polygon
|
||||
// CounterClockWise
|
||||
let ccw: boolean
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const area = this.signedArea(rings[i])
|
||||
if (area === 0) continue
|
||||
|
||||
if (ccw === undefined) {
|
||||
ccw = area < 0
|
||||
}
|
||||
if (ccw === (area < 0)) {
|
||||
if (polygon) {
|
||||
polygons.push(polygon)
|
||||
}
|
||||
polygon = [rings[i]]
|
||||
|
||||
} else {
|
||||
polygon.push(rings[i])
|
||||
}
|
||||
}
|
||||
if (polygon) {
|
||||
polygons.push(polygon)
|
||||
}
|
||||
|
||||
return polygons
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline replacement of the location by projecting
|
||||
* @param line
|
||||
* @private
|
||||
* @param line the line which will be rewritten inline
|
||||
* @return line
|
||||
*/
|
||||
private project(line: [number, number][]) {
|
||||
private project(line: Coords) {
|
||||
const y0 = this._y0
|
||||
const x0 = this._x0
|
||||
const size = this._size
|
||||
|
@ -174,12 +195,13 @@ class MvtFeatureBuilder {
|
|||
360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90,
|
||||
]
|
||||
}
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
export default class MvtSource implements FeatureSource {
|
||||
export default class MvtSource implements FeatureSourceForTile {
|
||||
|
||||
private static readonly schemaSpec = `
|
||||
private static readonly schemaSpec21 = `
|
||||
package vector_tile;
|
||||
|
||||
option optimize_for = LITE_RUNTIME;
|
||||
|
@ -259,26 +281,30 @@ message Tile {
|
|||
extensions 16 to 8191;
|
||||
}
|
||||
`
|
||||
private static readonly tile_schema = pbfCompile(PbfSchema.parse(MvtSource.schemaSpec)).Tile
|
||||
|
||||
|
||||
private static readonly tile_schema = (pbfCompile.default ?? pbfCompile)(PbfSchema.parse(MvtSource.schemaSpec21)).Tile
|
||||
public readonly features: Store<Feature<Geometry, { [name: string]: any }>[]>
|
||||
private readonly _url: string
|
||||
private readonly _layerName: string
|
||||
private readonly _features: UIEventSource<Feature<Geometry, {
|
||||
[name: string]: any
|
||||
}>[]> = new UIEventSource<Feature<Geometry, { [p: string]: any }>[]>([])
|
||||
public readonly features: Store<Feature<Geometry, { [name: string]: any }>[]> = this._features
|
||||
private readonly x: number
|
||||
private readonly y: number
|
||||
private readonly z: number
|
||||
public readonly x: number
|
||||
public readonly y: number
|
||||
public readonly z: number
|
||||
|
||||
constructor(url: string, x: number, y: number, z: number, layerName?: string) {
|
||||
constructor(url: string, x: number, y: number, z: number, layerName?: string, isActive?: Store<boolean>) {
|
||||
this._url = url
|
||||
this._layerName = layerName
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.z = z
|
||||
this.downloadSync()
|
||||
this.features = this._features.map(fs => {
|
||||
if (fs === undefined || isActive?.data === false) {
|
||||
return []
|
||||
}
|
||||
return fs
|
||||
}, [isActive])
|
||||
}
|
||||
|
||||
private getValue(v: {
|
||||
|
@ -316,16 +342,23 @@ message Tile {
|
|||
|
||||
}
|
||||
|
||||
private downloadSync(){
|
||||
private downloadSync() {
|
||||
this.download().then(d => {
|
||||
if(d.length === 0){
|
||||
if (d.length === 0) {
|
||||
return
|
||||
}
|
||||
return this._features.setData(d)
|
||||
}).catch(e => {console.error(e)})
|
||||
}).catch(e => {
|
||||
console.error(e)
|
||||
})
|
||||
}
|
||||
|
||||
private async download(): Promise<Feature[]> {
|
||||
const result = await fetch(this._url)
|
||||
if (result.status !== 200) {
|
||||
console.error("Could not download tile " + this._url)
|
||||
return []
|
||||
}
|
||||
const buffer = await result.arrayBuffer()
|
||||
const data = MvtSource.tile_schema.read(new Pbf(buffer))
|
||||
const layers = data.layers
|
||||
|
@ -336,7 +369,7 @@ message Tile {
|
|||
}
|
||||
layer = layers.find(l => l.name === this._layerName)
|
||||
}
|
||||
if(!layer){
|
||||
if (!layer) {
|
||||
return []
|
||||
}
|
||||
const builder = new MvtFeatureBuilder(layer.extent, this.x, this.y, this.z)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Store } from "../../UIEventSource"
|
||||
import { ImmutableStore, Store } from "../../UIEventSource"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import { Utils } from "../../../Utils"
|
||||
import GeoJsonSource from "../Sources/GeoJsonSource"
|
||||
|
@ -65,7 +65,7 @@ export default class DynamicGeoJsonTileSource extends DynamicTileSource {
|
|||
|
||||
const blackList = new Set<string>()
|
||||
super(
|
||||
source.geojsonZoomLevel,
|
||||
new ImmutableStore(source.geojsonZoomLevel),
|
||||
layer.minzoom,
|
||||
(zxy) => {
|
||||
if (whitelist !== undefined) {
|
||||
|
|
|
@ -1,13 +1,45 @@
|
|||
import { Store } from "../../UIEventSource"
|
||||
import DynamicTileSource from "./DynamicTileSource"
|
||||
import DynamicTileSource, { PolygonSourceMerger } from "./DynamicTileSource"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { BBox } from "../../BBox"
|
||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||
import MvtSource from "../Sources/MvtSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import Constants from "../../../Models/Constants"
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||
|
||||
export default class DynamicMvtileSource extends DynamicTileSource {
|
||||
|
||||
class PolygonMvtSource extends PolygonSourceMerger{
|
||||
constructor( layer: LayerConfig,
|
||||
mapProperties: {
|
||||
zoom: Store<number>
|
||||
bounds: Store<BBox>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}) {
|
||||
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: "polygons",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
mapProperties,
|
||||
{
|
||||
isActive: options?.isActive,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class PointMvtSource extends DynamicTileSource {
|
||||
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
|
@ -19,14 +51,16 @@ export default class DynamicMvtileSource extends DynamicTileSource {
|
|||
isActive?: Store<boolean>
|
||||
},
|
||||
) {
|
||||
const roundedZoom = mapProperties.zoom.mapD(z => Math.min(Math.floor(z/2)*2, 14))
|
||||
super(
|
||||
mapProperties.zoom,
|
||||
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: "pois",
|
||||
})
|
||||
return new MvtSource(url, x, y, z)
|
||||
},
|
||||
|
@ -37,3 +71,24 @@ export default class DynamicMvtileSource extends DynamicTileSource {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default class DynamicMvtileSource extends FeatureSourceMerger {
|
||||
|
||||
constructor(
|
||||
layer: LayerConfig,
|
||||
mapProperties: {
|
||||
zoom: Store<number>
|
||||
bounds: Store<BBox>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
},
|
||||
) {
|
||||
const roundedZoom = mapProperties.zoom.mapD(z => Math.floor(z))
|
||||
super(
|
||||
new PointMvtSource(layer, mapProperties, options),
|
||||
new PolygonMvtSource(layer, mapProperties, options)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +1,37 @@
|
|||
import { Store, Stores } from "../../UIEventSource"
|
||||
import { Tiles } from "../../../Models/TileRange"
|
||||
import { BBox } from "../../BBox"
|
||||
import { FeatureSource } from "../FeatureSource"
|
||||
import { FeatureSource, FeatureSourceForTile } from "../FeatureSource"
|
||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||
import { Feature } from "geojson"
|
||||
import { Utils } from "../../../Utils"
|
||||
import { GeoOperations } from "../../GeoOperations"
|
||||
|
||||
|
||||
/***
|
||||
* A tiled source which dynamically loads the required tiles at a fixed zoom level.
|
||||
* A single featureSource will be initialized for every tile in view; which will later be merged into this featureSource
|
||||
*/
|
||||
export default class DynamicTileSource extends FeatureSourceMerger {
|
||||
export default class DynamicTileSource<Src extends FeatureSource = FeatureSource> extends FeatureSourceMerger<Src> {
|
||||
/**
|
||||
*
|
||||
* @param zoomlevel If {z} is specified in the source, the 'zoomlevel' will be used as zoomlevel to download from
|
||||
* @param minzoom Only activate this feature source if zoomed in further then this
|
||||
* @param constructSource
|
||||
* @param mapProperties
|
||||
* @param options
|
||||
*/
|
||||
constructor(
|
||||
zoomlevel: Store<number>,
|
||||
minzoom: number,
|
||||
constructSource: (tileIndex: number) => FeatureSource,
|
||||
constructSource: (tileIndex: number) => Src,
|
||||
mapProperties: {
|
||||
bounds: Store<BBox>
|
||||
zoom: Store<number>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
}
|
||||
},
|
||||
) {
|
||||
super()
|
||||
const loadedTiles = new Set<number>()
|
||||
|
@ -34,32 +46,32 @@ export default class DynamicTileSource extends FeatureSourceMerger {
|
|||
if (mapProperties.zoom.data < minzoom) {
|
||||
return undefined
|
||||
}
|
||||
const z = Math.round(zoomlevel.data)
|
||||
const z = Math.floor(zoomlevel.data)
|
||||
const tileRange = Tiles.TileRangeBetween(
|
||||
z,
|
||||
bounds.getNorth(),
|
||||
bounds.getEast(),
|
||||
bounds.getSouth(),
|
||||
bounds.getWest()
|
||||
bounds.getWest(),
|
||||
)
|
||||
if (tileRange.total > 500) {
|
||||
console.warn(
|
||||
"Got a really big tilerange, bounds and location might be out of sync"
|
||||
"Got a really big tilerange, bounds and location might be out of sync",
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
const needed = Tiles.MapRange(tileRange, (x, y) =>
|
||||
Tiles.tile_index(z, x, y)
|
||||
Tiles.tile_index(z, x, y),
|
||||
).filter((i) => !loadedTiles.has(i))
|
||||
if (needed.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
return needed
|
||||
},
|
||||
[options?.isActive, mapProperties.zoom]
|
||||
[options?.isActive, mapProperties.zoom],
|
||||
)
|
||||
.stabilized(250)
|
||||
.stabilized(250),
|
||||
)
|
||||
|
||||
neededTiles.addCallbackAndRunD((neededIndexes) => {
|
||||
|
@ -70,3 +82,70 @@ export default class DynamicTileSource extends FeatureSourceMerger {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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<FeatureSourceForTile> {
|
||||
constructor(
|
||||
zoomlevel: Store<number>,
|
||||
minzoom: number,
|
||||
constructSource: (tileIndex: number) => FeatureSourceForTile,
|
||||
mapProperties: {
|
||||
bounds: Store<BBox>
|
||||
zoom: Store<number>
|
||||
},
|
||||
options?: {
|
||||
isActive?: Store<boolean>
|
||||
},
|
||||
) {
|
||||
super(zoomlevel, minzoom, constructSource, mapProperties, options)
|
||||
}
|
||||
|
||||
protected addDataFromSources(sources: FeatureSourceForTile[]) {
|
||||
sources = Utils.NoNull(sources)
|
||||
const all: Map<string, Feature> = new Map()
|
||||
const zooms: Map<string, number> = 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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -303,7 +303,6 @@ class LineRenderingLayer {
|
|||
type: "FeatureCollection",
|
||||
features,
|
||||
},
|
||||
cluster: true,
|
||||
promoteId: "id",
|
||||
})
|
||||
const linelayer = this._layername + "_line"
|
||||
|
|
|
@ -4,20 +4,18 @@
|
|||
import MaplibreMap from "./Map/MaplibreMap.svelte"
|
||||
import { Map as MlMap } from "maplibre-gl"
|
||||
import { MapLibreAdaptor } from "./Map/MapLibreAdaptor"
|
||||
import Constants from "../Models/Constants"
|
||||
import toilet from "../assets/generated/layers/toilet.json"
|
||||
import shops from "../assets/generated/layers/shops.json"
|
||||
import LayerConfig from "../Models/ThemeConfig/LayerConfig"
|
||||
import DynamicMvtileSource from "../Logic/FeatureSource/TiledFeatureSource/DynamicMvtTileSource"
|
||||
import ShowDataLayer from "./Map/ShowDataLayer"
|
||||
|
||||
const tl = new LayerConfig(<any>toilet)
|
||||
const tl = new LayerConfig(<any>shops)
|
||||
|
||||
|
||||
let map: UIEventSource<MlMap> = new UIEventSource<MlMap>(undefined)
|
||||
let adaptor = new MapLibreAdaptor(map)
|
||||
|
||||
const src = new DynamicMvtileSource(tl, adaptor)
|
||||
src.features.addCallbackAndRun(f => console.log(">>> Features are", f))
|
||||
new ShowDataLayer(map, {
|
||||
layer: tl,
|
||||
features: src
|
||||
|
@ -27,94 +25,7 @@
|
|||
lat: 51.2095, lon: 3.2260,
|
||||
})
|
||||
adaptor.zoom.setData(13)
|
||||
const loadedIcons = new Set<string>()
|
||||
|
||||
async function loadImage(map: MlMap, url: string, name: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (loadedIcons.has(name)) {
|
||||
return new Promise<void>((resolve, reject) => resolve())
|
||||
}
|
||||
loadedIcons.add(name)
|
||||
if (Constants.defaultPinIcons.indexOf(url) >= 0) {
|
||||
url = "./assets/svg/" + url + ".svg"
|
||||
}
|
||||
map.loadImage(
|
||||
url,
|
||||
(error, image) => {
|
||||
if (error) {
|
||||
reject(error)
|
||||
}
|
||||
map.addImage(name, image)
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
map.addCallbackAndRunD(map => {
|
||||
map.on("load", async () => {
|
||||
console.log("Onload")
|
||||
await loadImage(map, "https://upload.wikimedia.org/wikipedia/commons/7/7c/201408_cat.png", "cat")
|
||||
|
||||
/*
|
||||
map.addSource("drinking_water", {
|
||||
"type": "vector",
|
||||
"tiles": ["http://127.0.0.2:7800/public.drinking_water/{z}/{x}/{y}.pbf"], // http://127.0.0.2:7800/public.drinking_water.json",
|
||||
})
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
"id": "drinking_water_layer",
|
||||
"type": "circle",
|
||||
"source": "drinking_water",
|
||||
"source-layer": "public.drinking_water",
|
||||
"paint": {
|
||||
"circle-radius": 5,
|
||||
"circle-color": "#ff00ff",
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#000000",
|
||||
},
|
||||
},
|
||||
)*/
|
||||
/*
|
||||
map.addSource("toilet", {
|
||||
"type": "vector",
|
||||
"tiles": ["http://127.0.0.2:7800/public.toilet/{z}/{x}/{y}.pbf"], // http://127.0.0.2:7800/public.drinking_water.json",
|
||||
})
|
||||
|
||||
map.addLayer(
|
||||
{
|
||||
"id": "toilet_layer",
|
||||
"type": "circle",
|
||||
"source": "toilet",
|
||||
"source-layer": "public.toilet",
|
||||
"paint": {
|
||||
"circle-radius": 5,
|
||||
"circle-color": "#0000ff",
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#000000",
|
||||
},
|
||||
},
|
||||
)
|
||||
map.addLayer({
|
||||
"id": "points",
|
||||
"type": "symbol",
|
||||
"source": "toilet",
|
||||
"source-layer": "public.toilet",
|
||||
"layout": {
|
||||
"icon-overlap": "always",
|
||||
"icon-image": "cat",
|
||||
"icon-size": 0.05,
|
||||
},
|
||||
})*/
|
||||
|
||||
|
||||
map.on("click", "drinking_water_layer", (e) => {
|
||||
// Copy coordinates array.
|
||||
console.log(e)
|
||||
console.warn(">>>", e.features[0])
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="h-screen w-screen">
|
||||
|
|
Loading…
Reference in a new issue