forked from MapComplete/MapComplete
LayerServer: fix some bugs in build_db-script, decode MVT-tilesource for more features
This commit is contained in:
parent
ee3e000cd1
commit
5b318236bf
8 changed files with 327 additions and 103 deletions
|
@ -7,6 +7,7 @@ import { Or } from "../../src/Logic/Tags/Or"
|
||||||
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
|
import { RegexTag } from "../../src/Logic/Tags/RegexTag"
|
||||||
import { ValidateThemeEnsemble } from "../../src/Models/ThemeConfig/Conversion/Validation"
|
import { ValidateThemeEnsemble } from "../../src/Models/ThemeConfig/Conversion/Validation"
|
||||||
import { AllKnownLayouts } from "../../src/Customizations/AllKnownLayouts"
|
import { AllKnownLayouts } from "../../src/Customizations/AllKnownLayouts"
|
||||||
|
import { OsmObject } from "../../src/Logic/Osm/OsmObject"
|
||||||
|
|
||||||
class LuaSnippets {
|
class LuaSnippets {
|
||||||
|
|
||||||
|
@ -20,6 +21,33 @@ class LuaSnippets {
|
||||||
"end",
|
"end",
|
||||||
].join("\n")
|
].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 {
|
public static toLuaFilter(tag: TagsFilter, useParens: boolean = false): string {
|
||||||
if (tag instanceof Tag) {
|
if (tag instanceof Tag) {
|
||||||
return `object.tags["${tag.key}"] == "${tag.value}"`
|
return `object.tags["${tag.key}"] == "${tag.value}"`
|
||||||
|
@ -55,6 +83,10 @@ class LuaSnippets {
|
||||||
return `object.tags["${tag.key}"] ~= "${tag.value}"`
|
return `object.tags["${tag.key}"] ~= "${tag.value}"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if ("" + tag.value === "/.+/is" && !tag.invert) {
|
||||||
|
@ -220,6 +252,7 @@ class GenerateBuildDbScript extends Script {
|
||||||
bodyPolygons.push(this.insertInto(tags, layerId, "polygons_").join("\n"))
|
bodyPolygons.push(this.insertInto(tags, layerId, "polygons_").join("\n"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isPolygon = LuaSnippets.isPolygonFeature()
|
||||||
return [
|
return [
|
||||||
"function process_polygon(object, geom)",
|
"function process_polygon(object, geom)",
|
||||||
" local matches_filter",
|
" local matches_filter",
|
||||||
|
@ -232,7 +265,9 @@ class GenerateBuildDbScript extends Script {
|
||||||
"",
|
"",
|
||||||
"function osm2pgsql.process_way(object)",
|
"function osm2pgsql.process_way(object)",
|
||||||
this.earlyAbort(),
|
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())",
|
" process_polygon(object, object:as_polygon())",
|
||||||
" else",
|
" else",
|
||||||
" process_linestring(object, object:as_linestring())",
|
" process_linestring(object, object:as_linestring())",
|
||||||
|
|
|
@ -59,7 +59,7 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
zoom,
|
zoom,
|
||||||
featureSwitches
|
featureSwitches
|
||||||
)//*/
|
)//*/
|
||||||
|
/*
|
||||||
const osmApiSource = LayoutSource.setupOsmApiSource(
|
const osmApiSource = LayoutSource.setupOsmApiSource(
|
||||||
osmLayers,
|
osmLayers,
|
||||||
bounds,
|
bounds,
|
||||||
|
@ -67,14 +67,14 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
backend,
|
backend,
|
||||||
featureSwitches,
|
featureSwitches,
|
||||||
fullNodeDatabaseSource
|
fullNodeDatabaseSource
|
||||||
)
|
)*/
|
||||||
|
|
||||||
const geojsonSources: FeatureSource[] = geojsonlayers.map((l) =>
|
const geojsonSources: FeatureSource[] = geojsonlayers.map((l) =>
|
||||||
LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
|
LayoutSource.setupGeojsonSource(l, mapProperties, isDisplayed(l.id))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
super(osmApiSource, ...geojsonSources, ...fromCache, ...mvtSources)
|
super(...geojsonSources, ...fromCache, ...mvtSources)
|
||||||
|
|
||||||
const self = this
|
const self = this
|
||||||
function setIsLoading() {
|
function setIsLoading() {
|
||||||
|
@ -83,7 +83,7 @@ export default class LayoutSource extends FeatureSourceMerger {
|
||||||
}
|
}
|
||||||
|
|
||||||
// overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading())
|
// overpassSource?.runningQuery?.addCallbackAndRun((_) => setIsLoading())
|
||||||
osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading())
|
// osmApiSource?.isRunning?.addCallbackAndRun((_) => setIsLoading())
|
||||||
}
|
}
|
||||||
|
|
||||||
private static setupMvtSource(layer: LayerConfig, mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, isActive?: Store<boolean>): FeatureSource{
|
private static setupMvtSource(layer: LayerConfig, mapProperties: { zoom: Store<number>; bounds: Store<BBox> }, isActive?: Store<boolean>): FeatureSource{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Store } from "../../UIEventSource"
|
import { Store } from "../../UIEventSource"
|
||||||
import DynamicTileSource, { PolygonSourceMerger } from "./DynamicTileSource"
|
import DynamicTileSource from "./DynamicTileSource"
|
||||||
import { Utils } from "../../../Utils"
|
import { Utils } from "../../../Utils"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
import LayerConfig from "../../../Models/ThemeConfig/LayerConfig"
|
||||||
|
@ -7,6 +7,8 @@ import MvtSource from "../Sources/MvtSource"
|
||||||
import { Tiles } from "../../../Models/TileRange"
|
import { Tiles } from "../../../Models/TileRange"
|
||||||
import Constants from "../../../Models/Constants"
|
import Constants from "../../../Models/Constants"
|
||||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
||||||
|
import { LineSourceMerger } from "./LineSourceMerger"
|
||||||
|
import { PolygonSourceMerger } from "./PolygonSourceMerger"
|
||||||
|
|
||||||
|
|
||||||
class PolygonMvtSource extends PolygonSourceMerger{
|
class PolygonMvtSource extends PolygonSourceMerger{
|
||||||
|
@ -39,6 +41,36 @@ class PolygonMvtSource extends PolygonSourceMerger{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class LineMvtSource extends LineSourceMerger{
|
||||||
|
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: "lines",
|
||||||
|
})
|
||||||
|
return new MvtSource(url, x, y, z)
|
||||||
|
},
|
||||||
|
mapProperties,
|
||||||
|
{
|
||||||
|
isActive: options?.isActive,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PointMvtSource extends DynamicTileSource {
|
class PointMvtSource extends DynamicTileSource {
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -84,9 +116,9 @@ export default class DynamicMvtileSource extends FeatureSourceMerger {
|
||||||
isActive?: Store<boolean>
|
isActive?: Store<boolean>
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const roundedZoom = mapProperties.zoom.mapD(z => Math.floor(z))
|
|
||||||
super(
|
super(
|
||||||
new PointMvtSource(layer, mapProperties, options),
|
new PointMvtSource(layer, mapProperties, options),
|
||||||
|
new LineMvtSource(layer, mapProperties, options),
|
||||||
new PolygonMvtSource(layer, mapProperties, options)
|
new PolygonMvtSource(layer, mapProperties, options)
|
||||||
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
import { Store, Stores } from "../../UIEventSource"
|
import { Store, Stores } from "../../UIEventSource"
|
||||||
import { Tiles } from "../../../Models/TileRange"
|
import { Tiles } from "../../../Models/TileRange"
|
||||||
import { BBox } from "../../BBox"
|
import { BBox } from "../../BBox"
|
||||||
import { FeatureSource, FeatureSourceForTile } from "../FeatureSource"
|
import { FeatureSource } from "../FeatureSource"
|
||||||
import FeatureSourceMerger from "../Sources/FeatureSourceMerger"
|
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<Src extends FeatureSource = FeatureSource
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
@ -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<FeatureSourceForTile> {
|
||||||
|
private readonly _zoomlevel: Store<number>
|
||||||
|
|
||||||
|
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)
|
||||||
|
this._zoomlevel = zoomlevel
|
||||||
|
}
|
||||||
|
|
||||||
|
protected addDataFromSources(sources: FeatureSourceForTile[]) {
|
||||||
|
sources = Utils.NoNull(sources)
|
||||||
|
const all: Map<string, Feature<MultiLineString>> = 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, <any> GeoOperations.attemptLinearize(<Feature<MultiLineString>>all.get(key)))
|
||||||
|
}
|
||||||
|
const newList = Array.from(all.values())
|
||||||
|
this.features.setData(newList)
|
||||||
|
this._featuresById.setData(all)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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<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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { BBox } from "./BBox"
|
import { BBox } from "./BBox"
|
||||||
import * as turf from "@turf/turf"
|
import * as turf from "@turf/turf"
|
||||||
import { AllGeoJSON, booleanWithin, Coord } from "@turf/turf"
|
import { AllGeoJSON, booleanWithin, Coord, Lines } from "@turf/turf"
|
||||||
import {
|
import {
|
||||||
Feature,
|
Feature,
|
||||||
FeatureCollection,
|
FeatureCollection,
|
||||||
|
@ -156,7 +156,7 @@ export class GeoOperations {
|
||||||
const intersection = GeoOperations.calculateIntersection(
|
const intersection = GeoOperations.calculateIntersection(
|
||||||
feature,
|
feature,
|
||||||
otherFeature,
|
otherFeature,
|
||||||
featureBBox
|
featureBBox,
|
||||||
)
|
)
|
||||||
if (intersection === null) {
|
if (intersection === null) {
|
||||||
continue
|
continue
|
||||||
|
@ -195,7 +195,7 @@ export class GeoOperations {
|
||||||
console.error(
|
console.error(
|
||||||
"Could not correctly calculate the overlap of ",
|
"Could not correctly calculate the overlap of ",
|
||||||
feature,
|
feature,
|
||||||
": unsupported type"
|
": unsupported type",
|
||||||
)
|
)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -224,7 +224,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static inside(
|
public static inside(
|
||||||
pointCoordinate: [number, number] | Feature<Point>,
|
pointCoordinate: [number, number] | Feature<Point>,
|
||||||
feature: Feature
|
feature: Feature,
|
||||||
): boolean {
|
): boolean {
|
||||||
// ray-casting algorithm based on
|
// ray-casting algorithm based on
|
||||||
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
// http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
|
||||||
|
@ -302,7 +302,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static nearestPoint(
|
public static nearestPoint(
|
||||||
way: Feature<LineString>,
|
way: Feature<LineString>,
|
||||||
point: [number, number]
|
point: [number, number],
|
||||||
): Feature<
|
): Feature<
|
||||||
Point,
|
Point,
|
||||||
{
|
{
|
||||||
|
@ -324,11 +324,11 @@ export class GeoOperations {
|
||||||
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
|
public static forceLineString(way: Feature<LineString | Polygon>): Feature<LineString>
|
||||||
|
|
||||||
public static forceLineString(
|
public static forceLineString(
|
||||||
way: Feature<MultiLineString | MultiPolygon>
|
way: Feature<MultiLineString | MultiPolygon>,
|
||||||
): Feature<MultiLineString>
|
): Feature<MultiLineString>
|
||||||
|
|
||||||
public static forceLineString(
|
public static forceLineString(
|
||||||
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
way: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
): Feature<LineString | MultiLineString> {
|
): Feature<LineString | MultiLineString> {
|
||||||
if (way.geometry.type === "Polygon") {
|
if (way.geometry.type === "Polygon") {
|
||||||
way = { ...way }
|
way = { ...way }
|
||||||
|
@ -448,7 +448,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static LineIntersections(
|
public static LineIntersections(
|
||||||
feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
feature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>
|
otherFeature: Feature<LineString | MultiLineString | Polygon | MultiPolygon>,
|
||||||
): [number, number][] {
|
): [number, number][] {
|
||||||
return turf
|
return turf
|
||||||
.lineIntersect(feature, otherFeature)
|
.lineIntersect(feature, otherFeature)
|
||||||
|
@ -485,7 +485,7 @@ export class GeoOperations {
|
||||||
locations:
|
locations:
|
||||||
| Feature<LineString>
|
| Feature<LineString>
|
||||||
| Feature<Point, { date?: string; altitude?: number | string }>[],
|
| Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||||
title?: string
|
title?: string,
|
||||||
) {
|
) {
|
||||||
title = title?.trim()
|
title = title?.trim()
|
||||||
if (title === undefined || title === "") {
|
if (title === undefined || title === "") {
|
||||||
|
@ -506,7 +506,7 @@ export class GeoOperations {
|
||||||
type: "Point",
|
type: "Point",
|
||||||
coordinates: p,
|
coordinates: p,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
for (const l of locationsWithMeta) {
|
for (const l of locationsWithMeta) {
|
||||||
|
@ -521,7 +521,7 @@ export class GeoOperations {
|
||||||
trackPoints.push(trkpt)
|
trackPoints.push(trkpt)
|
||||||
}
|
}
|
||||||
const header =
|
const header =
|
||||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
|
||||||
return (
|
return (
|
||||||
header +
|
header +
|
||||||
"\n<name>" +
|
"\n<name>" +
|
||||||
|
@ -539,7 +539,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static toGpxPoints(
|
public static toGpxPoints(
|
||||||
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
|
locations: Feature<Point, { date?: string; altitude?: number | string }>[],
|
||||||
title?: string
|
title?: string,
|
||||||
) {
|
) {
|
||||||
title = title?.trim()
|
title = title?.trim()
|
||||||
if (title === undefined || title === "") {
|
if (title === undefined || title === "") {
|
||||||
|
@ -560,7 +560,7 @@ export class GeoOperations {
|
||||||
trackPoints.push(trkpt)
|
trackPoints.push(trkpt)
|
||||||
}
|
}
|
||||||
const header =
|
const header =
|
||||||
'<gpx version="1.1" creator="mapcomplete.org" xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd">'
|
"<gpx version=\"1.1\" creator=\"mapcomplete.org\" xmlns=\"http://www.topografix.com/GPX/1/1\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd\">"
|
||||||
return (
|
return (
|
||||||
header +
|
header +
|
||||||
"\n<name>" +
|
"\n<name>" +
|
||||||
|
@ -648,7 +648,7 @@ export class GeoOperations {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
distanceMeter,
|
distanceMeter,
|
||||||
{ units: "meters" }
|
{ units: "meters" },
|
||||||
).geometry.coordinates
|
).geometry.coordinates
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -683,7 +683,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
static completelyWithin(
|
static completelyWithin(
|
||||||
feature: Feature<Geometry, any>,
|
feature: Feature<Geometry, any>,
|
||||||
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>
|
possiblyEnclosingFeature: Feature<Polygon | MultiPolygon, any>,
|
||||||
): boolean {
|
): boolean {
|
||||||
return booleanWithin(feature, possiblyEnclosingFeature)
|
return booleanWithin(feature, possiblyEnclosingFeature)
|
||||||
}
|
}
|
||||||
|
@ -714,6 +714,23 @@ export class GeoOperations {
|
||||||
}
|
}
|
||||||
return kept
|
return kept
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (toSplit.geometry.type === "MultiLineString") {
|
||||||
|
const lines: Feature<LineString>[][] = toSplit.geometry.coordinates.map(coordinates =>
|
||||||
|
turf.lineSplit(<LineString> {type: "LineString", coordinates}, boundary).features )
|
||||||
|
const splitted: Feature<LineString>[] = [].concat(...lines)
|
||||||
|
const kept: Feature<LineString>[] = []
|
||||||
|
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") {
|
if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") {
|
||||||
const splitup = turf.intersect(<Feature<Polygon>>toSplit, boundary)
|
const splitup = turf.intersect(<Feature<Polygon>>toSplit, boundary)
|
||||||
splitup.properties = { ...toSplit.properties }
|
splitup.properties = { ...toSplit.properties }
|
||||||
|
@ -739,7 +756,7 @@ export class GeoOperations {
|
||||||
*/
|
*/
|
||||||
public static featureToCoordinateWithRenderingType(
|
public static featureToCoordinateWithRenderingType(
|
||||||
feature: Feature,
|
feature: Feature,
|
||||||
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string
|
location: "point" | "centroid" | "start" | "end" | "projected_centerpoint" | string,
|
||||||
): [number, number] | undefined {
|
): [number, number] | undefined {
|
||||||
switch (location) {
|
switch (location) {
|
||||||
case "point":
|
case "point":
|
||||||
|
@ -760,7 +777,7 @@ export class GeoOperations {
|
||||||
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
const centerpoint = GeoOperations.centerpointCoordinates(feature)
|
||||||
const projected = GeoOperations.nearestPoint(
|
const projected = GeoOperations.nearestPoint(
|
||||||
<Feature<LineString>>feature,
|
<Feature<LineString>>feature,
|
||||||
centerpoint
|
centerpoint,
|
||||||
)
|
)
|
||||||
return <[number, number]>projected.geometry.coordinates
|
return <[number, number]>projected.geometry.coordinates
|
||||||
}
|
}
|
||||||
|
@ -937,7 +954,7 @@ export class GeoOperations {
|
||||||
* GeoOperations.bearingToHuman(46) // => "NE"
|
* GeoOperations.bearingToHuman(46) // => "NE"
|
||||||
*/
|
*/
|
||||||
public static bearingToHuman(
|
public static bearingToHuman(
|
||||||
bearing: number
|
bearing: number,
|
||||||
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
|
): "N" | "NE" | "E" | "SE" | "S" | "SW" | "W" | "NW" {
|
||||||
while (bearing < 0) {
|
while (bearing < 0) {
|
||||||
bearing += 360
|
bearing += 360
|
||||||
|
@ -956,7 +973,7 @@ export class GeoOperations {
|
||||||
* GeoOperations.bearingToHuman(46) // => "NE"
|
* GeoOperations.bearingToHuman(46) // => "NE"
|
||||||
*/
|
*/
|
||||||
public static bearingToHumanRelative(
|
public static bearingToHumanRelative(
|
||||||
bearing: number
|
bearing: number,
|
||||||
):
|
):
|
||||||
| "straight"
|
| "straight"
|
||||||
| "slight_right"
|
| "slight_right"
|
||||||
|
@ -975,18 +992,73 @@ export class GeoOperations {
|
||||||
return GeoOperations.directionsRelative[segment]
|
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 = <any> {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<MultiLineString>): Feature<LineString | MultiLineString> {
|
||||||
|
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'
|
* Helper function which does the heavy lifting for 'inside'
|
||||||
*/
|
*/
|
||||||
private static pointInPolygonCoordinates(
|
private static pointInPolygonCoordinates(
|
||||||
x: number,
|
x: number,
|
||||||
y: number,
|
y: number,
|
||||||
coordinates: [number, number][][]
|
coordinates: [number, number][][],
|
||||||
): boolean {
|
): boolean {
|
||||||
const inside = GeoOperations.pointWithinRing(
|
const inside = GeoOperations.pointWithinRing(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
/*This is the outer ring of the polygon */ coordinates[0]
|
/*This is the outer ring of the polygon */ coordinates[0],
|
||||||
)
|
)
|
||||||
if (!inside) {
|
if (!inside) {
|
||||||
return false
|
return false
|
||||||
|
@ -995,7 +1067,7 @@ export class GeoOperations {
|
||||||
const inHole = GeoOperations.pointWithinRing(
|
const inHole = GeoOperations.pointWithinRing(
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
coordinates[i] /* These are inner rings, aka holes*/
|
coordinates[i], /* These are inner rings, aka holes*/
|
||||||
)
|
)
|
||||||
if (inHole) {
|
if (inHole) {
|
||||||
return false
|
return false
|
||||||
|
@ -1033,7 +1105,7 @@ export class GeoOperations {
|
||||||
feature,
|
feature,
|
||||||
otherFeature,
|
otherFeature,
|
||||||
featureBBox: BBox,
|
featureBBox: BBox,
|
||||||
otherFeatureBBox?: BBox
|
otherFeatureBBox?: BBox,
|
||||||
): number {
|
): number {
|
||||||
if (feature.geometry.type === "LineString") {
|
if (feature.geometry.type === "LineString") {
|
||||||
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature)
|
otherFeatureBBox = otherFeatureBBox ?? BBox.get(otherFeature)
|
||||||
|
@ -1082,7 +1154,7 @@ export class GeoOperations {
|
||||||
let intersection = turf.lineSlice(
|
let intersection = turf.lineSlice(
|
||||||
turf.point(intersectionPointsArray[0]),
|
turf.point(intersectionPointsArray[0]),
|
||||||
turf.point(intersectionPointsArray[1]),
|
turf.point(intersectionPointsArray[1]),
|
||||||
feature
|
feature,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (intersection == null) {
|
if (intersection == null) {
|
||||||
|
@ -1103,7 +1175,7 @@ export class GeoOperations {
|
||||||
otherFeature,
|
otherFeature,
|
||||||
feature,
|
feature,
|
||||||
otherFeatureBBox,
|
otherFeatureBBox,
|
||||||
featureBBox
|
featureBBox,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1123,7 +1195,7 @@ export class GeoOperations {
|
||||||
console.log("Applying fallback intersection...")
|
console.log("Applying fallback intersection...")
|
||||||
const intersection = turf.intersect(
|
const intersection = turf.intersect(
|
||||||
turf.truncate(feature),
|
turf.truncate(feature),
|
||||||
turf.truncate(otherFeature)
|
turf.truncate(otherFeature),
|
||||||
)
|
)
|
||||||
if (intersection == null) {
|
if (intersection == null) {
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -7,7 +7,7 @@ import { Feature, LineString, Polygon } from "geojson"
|
||||||
export abstract class OsmObject {
|
export abstract class OsmObject {
|
||||||
private static defaultBackend = "https://api.openstreetmap.org/"
|
private static defaultBackend = "https://api.openstreetmap.org/"
|
||||||
protected static backendURL = OsmObject.defaultBackend
|
protected static backendURL = OsmObject.defaultBackend
|
||||||
private static polygonFeatures = OsmObject.constructPolygonFeatures()
|
public static polygonFeatures = OsmObject.constructPolygonFeatures()
|
||||||
type: "node" | "way" | "relation"
|
type: "node" | "way" | "relation"
|
||||||
id: number
|
id: number
|
||||||
/**
|
/**
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue