Add clipping to generateCache
This commit is contained in:
parent
f7f0ccdb7d
commit
509b237d02
3 changed files with 177 additions and 90 deletions
|
@ -15,6 +15,11 @@ import togpx from "togpx"
|
||||||
import Constants from "../Models/Constants"
|
import Constants from "../Models/Constants"
|
||||||
|
|
||||||
export class GeoOperations {
|
export class GeoOperations {
|
||||||
|
/**
|
||||||
|
* Create a union between two features
|
||||||
|
*/
|
||||||
|
static union = turf.union
|
||||||
|
static intersect = turf.intersect
|
||||||
private static readonly _earthRadius = 6378137
|
private static readonly _earthRadius = 6378137
|
||||||
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
|
private static readonly _originShift = (2 * Math.PI * GeoOperations._earthRadius) / 2
|
||||||
|
|
||||||
|
@ -158,35 +163,6 @@ export class GeoOperations {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function which does the heavy lifting for 'inside'
|
|
||||||
*/
|
|
||||||
private static pointInPolygonCoordinates(
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
coordinates: [number, number][][]
|
|
||||||
) {
|
|
||||||
const inside = GeoOperations.pointWithinRing(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
/*This is the outer ring of the polygon */ coordinates[0]
|
|
||||||
)
|
|
||||||
if (!inside) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for (let i = 1; i < coordinates.length; i++) {
|
|
||||||
const inHole = GeoOperations.pointWithinRing(
|
|
||||||
x,
|
|
||||||
y,
|
|
||||||
coordinates[i] /* These are inner rings, aka holes*/
|
|
||||||
)
|
|
||||||
if (inHole) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect wether or not the given point is located in the feature
|
* Detect wether or not the given point is located in the feature
|
||||||
*
|
*
|
||||||
|
@ -620,6 +596,113 @@ export class GeoOperations {
|
||||||
return copy
|
return copy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees)
|
||||||
|
*/
|
||||||
|
public static bearing(a: Coord, b: Coord): number {
|
||||||
|
return turf.bearing(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 'true' if one feature contains the other feature
|
||||||
|
*
|
||||||
|
* const pond: Feature<Polygon, any> = {
|
||||||
|
* "type": "Feature",
|
||||||
|
* "properties": {"natural":"water","water":"pond"},
|
||||||
|
* "geometry": {
|
||||||
|
* "type": "Polygon",
|
||||||
|
* "coordinates": [[
|
||||||
|
* [4.362924098968506,50.8435422298544 ],
|
||||||
|
* [4.363272786140442,50.8435219059949 ],
|
||||||
|
* [4.363213777542114,50.8437420806679 ],
|
||||||
|
* [4.362924098968506,50.8435422298544 ]
|
||||||
|
* ]]}}
|
||||||
|
* const park: Feature<Polygon, any> = {
|
||||||
|
* "type": "Feature",
|
||||||
|
* "properties": {"leisure":"park"},
|
||||||
|
* "geometry": {
|
||||||
|
* "type": "Polygon",
|
||||||
|
* "coordinates": [[
|
||||||
|
* [ 4.36073541641235,50.84323737103244 ],
|
||||||
|
* [ 4.36469435691833, 50.8423905305197 ],
|
||||||
|
* [ 4.36659336090087, 50.8458997374786 ],
|
||||||
|
* [ 4.36254858970642, 50.8468007074916 ],
|
||||||
|
* [ 4.36073541641235, 50.8432373710324 ]
|
||||||
|
* ]]}}
|
||||||
|
* GeoOperations.completelyWithin(pond, park) // => true
|
||||||
|
* GeoOperations.completelyWithin(park, pond) // => false
|
||||||
|
*/
|
||||||
|
static completelyWithin(
|
||||||
|
feature: Feature<Geometry, any>,
|
||||||
|
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>
|
||||||
|
): boolean {
|
||||||
|
return booleanWithin(feature, possiblyEncloingFeature)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an intersection between two features.
|
||||||
|
* A new feature is returned based on 'toSplit', which'll have a geometry that is completely withing boundary
|
||||||
|
*/
|
||||||
|
public static clipWith(toSplit: Feature, boundary: Feature<Polygon>): Feature[] {
|
||||||
|
if (toSplit.geometry.type === "Point") {
|
||||||
|
const p = <Feature<Point>>toSplit
|
||||||
|
if (GeoOperations.inside(p.geometry.coordinates, boundary)) {
|
||||||
|
return [p]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toSplit.geometry.type === "LineString") {
|
||||||
|
const splitup = turf.lineSplit(<Feature<LineString>>toSplit, boundary)
|
||||||
|
const kept = []
|
||||||
|
for (const f of splitup.features) {
|
||||||
|
const ls = <Feature<LineString>>f
|
||||||
|
if (!GeoOperations.inside(GeoOperations.centerpointCoordinates(f), boundary)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
f.properties = { ...toSplit.properties }
|
||||||
|
kept.push(f)
|
||||||
|
}
|
||||||
|
return kept
|
||||||
|
}
|
||||||
|
if (toSplit.geometry.type === "Polygon" || toSplit.geometry.type == "MultiPolygon") {
|
||||||
|
const splitup = turf.intersect(<Feature<Polygon>>toSplit, boundary)
|
||||||
|
splitup.properties = { ...toSplit.properties }
|
||||||
|
return [splitup]
|
||||||
|
}
|
||||||
|
throw "Invalid geometry type with GeoOperations.clipWith: " + toSplit.geometry.type
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function which does the heavy lifting for 'inside'
|
||||||
|
*/
|
||||||
|
private static pointInPolygonCoordinates(
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
coordinates: [number, number][][]
|
||||||
|
) {
|
||||||
|
const inside = GeoOperations.pointWithinRing(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
/*This is the outer ring of the polygon */ coordinates[0]
|
||||||
|
)
|
||||||
|
if (!inside) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (let i = 1; i < coordinates.length; i++) {
|
||||||
|
const inHole = GeoOperations.pointWithinRing(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
coordinates[i] /* These are inner rings, aka holes*/
|
||||||
|
)
|
||||||
|
if (inHole) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
|
private static pointWithinRing(x: number, y: number, ring: [number, number][]) {
|
||||||
let inside = false
|
let inside = false
|
||||||
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {
|
||||||
|
@ -740,57 +823,4 @@ export class GeoOperations {
|
||||||
}
|
}
|
||||||
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
throw "CalculateIntersection fallthrough: can not calculate an intersection between features"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Takes two points and finds the geographic bearing between them, i.e. the angle measured in degrees from the north line (0 degrees)
|
|
||||||
*/
|
|
||||||
public static bearing(a: Coord, b: Coord): number {
|
|
||||||
return turf.bearing(a, b)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns 'true' if one feature contains the other feature
|
|
||||||
*
|
|
||||||
* const pond: Feature<Polygon, any> = {
|
|
||||||
* "type": "Feature",
|
|
||||||
* "properties": {"natural":"water","water":"pond"},
|
|
||||||
* "geometry": {
|
|
||||||
* "type": "Polygon",
|
|
||||||
* "coordinates": [[
|
|
||||||
* [4.362924098968506,50.8435422298544 ],
|
|
||||||
* [4.363272786140442,50.8435219059949 ],
|
|
||||||
* [4.363213777542114,50.8437420806679 ],
|
|
||||||
* [4.362924098968506,50.8435422298544 ]
|
|
||||||
* ]]}}
|
|
||||||
* const park: Feature<Polygon, any> = {
|
|
||||||
* "type": "Feature",
|
|
||||||
* "properties": {"leisure":"park"},
|
|
||||||
* "geometry": {
|
|
||||||
* "type": "Polygon",
|
|
||||||
* "coordinates": [[
|
|
||||||
* [ 4.36073541641235,50.84323737103244 ],
|
|
||||||
* [ 4.36469435691833, 50.8423905305197 ],
|
|
||||||
* [ 4.36659336090087, 50.8458997374786 ],
|
|
||||||
* [ 4.36254858970642, 50.8468007074916 ],
|
|
||||||
* [ 4.36073541641235, 50.8432373710324 ]
|
|
||||||
* ]]}}
|
|
||||||
* GeoOperations.completelyWithin(pond, park) // => true
|
|
||||||
* GeoOperations.completelyWithin(park, pond) // => false
|
|
||||||
*/
|
|
||||||
static completelyWithin(
|
|
||||||
feature: Feature<Geometry, any>,
|
|
||||||
possiblyEncloingFeature: Feature<Polygon | MultiPolygon, any>
|
|
||||||
): boolean {
|
|
||||||
return booleanWithin(feature, possiblyEncloingFeature)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a union between two features
|
|
||||||
*/
|
|
||||||
static union = turf.union
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an intersection between two features
|
|
||||||
*/
|
|
||||||
static intersect = turf.intersect
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,9 @@ import { GeoOperations } from "../Logic/GeoOperations"
|
||||||
import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"
|
import SimpleMetaTaggers from "../Logic/SimpleMetaTagger"
|
||||||
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
import FilteringFeatureSource from "../Logic/FeatureSource/Sources/FilteringFeatureSource"
|
||||||
import Loc from "../Models/Loc"
|
import Loc from "../Models/Loc"
|
||||||
|
import { Feature } from "geojson"
|
||||||
|
import { BBox } from "../Logic/BBox"
|
||||||
|
import { bboxClip } from "@turf/turf"
|
||||||
|
|
||||||
ScriptUtils.fixUtils()
|
ScriptUtils.fixUtils()
|
||||||
|
|
||||||
|
@ -232,7 +235,8 @@ function sliceToTiles(
|
||||||
theme: LayoutConfig,
|
theme: LayoutConfig,
|
||||||
relationsTracker: RelationsTracker,
|
relationsTracker: RelationsTracker,
|
||||||
targetdir: string,
|
targetdir: string,
|
||||||
pointsOnlyLayers: string[]
|
pointsOnlyLayers: string[],
|
||||||
|
clip: boolean
|
||||||
) {
|
) {
|
||||||
const skippedLayers = new Set<string>()
|
const skippedLayers = new Set<string>()
|
||||||
|
|
||||||
|
@ -310,6 +314,7 @@ function sliceToTiles(
|
||||||
maxFeatureCount: undefined,
|
maxFeatureCount: undefined,
|
||||||
registerTile: (tile) => {
|
registerTile: (tile) => {
|
||||||
const tileIndex = tile.tileIndex
|
const tileIndex = tile.tileIndex
|
||||||
|
const bbox = BBox.fromTileIndex(tileIndex).asGeoJson({})
|
||||||
console.log("Got tile:", tileIndex, tile.layer.layerDef.id)
|
console.log("Got tile:", tileIndex, tile.layer.layerDef.id)
|
||||||
if (tile.features.data.length === 0) {
|
if (tile.features.data.length === 0) {
|
||||||
return
|
return
|
||||||
|
@ -343,9 +348,9 @@ function sliceToTiles(
|
||||||
}
|
}
|
||||||
let strictlyCalculated = 0
|
let strictlyCalculated = 0
|
||||||
let featureCount = 0
|
let featureCount = 0
|
||||||
for (const feature of filteredTile.features.data) {
|
let features: Feature[] = filteredTile.features.data.map((f) => f.feature)
|
||||||
|
for (const feature of features) {
|
||||||
// Some cleanup
|
// Some cleanup
|
||||||
delete feature.feature["bbox"]
|
|
||||||
|
|
||||||
if (tile.layer.layerDef.calculatedTags !== undefined) {
|
if (tile.layer.layerDef.calculatedTags !== undefined) {
|
||||||
// Evaluate all the calculated tags strictly
|
// Evaluate all the calculated tags strictly
|
||||||
|
@ -353,7 +358,7 @@ function sliceToTiles(
|
||||||
(ct) => ct[0]
|
(ct) => ct[0]
|
||||||
)
|
)
|
||||||
featureCount++
|
featureCount++
|
||||||
const props = feature.feature.properties
|
const props = feature.properties
|
||||||
for (const calculatedTagKey of calculatedTagKeys) {
|
for (const calculatedTagKey of calculatedTagKeys) {
|
||||||
const strict = props[calculatedTagKey]
|
const strict = props[calculatedTagKey]
|
||||||
|
|
||||||
|
@ -379,7 +384,16 @@ function sliceToTiles(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
delete feature["bbox"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (clip) {
|
||||||
|
console.log("Clipping features")
|
||||||
|
features = [].concat(
|
||||||
|
...features.map((f: Feature) => GeoOperations.clipWith(<any>f, bbox))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Lets save this tile!
|
// Lets save this tile!
|
||||||
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
const [z, x, y] = Tiles.tile_from_index(tileIndex)
|
||||||
// console.log("Writing tile ", z, x, y, layerId)
|
// console.log("Writing tile ", z, x, y, layerId)
|
||||||
|
@ -391,7 +405,7 @@ function sliceToTiles(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
type: "FeatureCollection",
|
type: "FeatureCollection",
|
||||||
features: filteredTile.features.data.map((f) => f.feature),
|
features,
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
" "
|
" "
|
||||||
|
@ -476,8 +490,9 @@ export async function main(args: string[]) {
|
||||||
console.log("Cache builder started with args ", args.join(", "))
|
console.log("Cache builder started with args ", args.join(", "))
|
||||||
if (args.length < 6) {
|
if (args.length < 6) {
|
||||||
console.error(
|
console.error(
|
||||||
"Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] \n" +
|
"Expected arguments are: theme zoomlevel targetdirectory lat0 lon0 lat1 lon1 [--generate-point-overview layer-name,layer-name,...] [--force-zoom-level z] [--clip]" +
|
||||||
"Note: a new directory named <theme> will be created in targetdirectory"
|
"--force-zoom-level causes non-cached-layers to be donwnloaded\n" +
|
||||||
|
"--clip will erase parts of the feature falling outside of the bounding box"
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -494,6 +509,7 @@ export async function main(args: string[]) {
|
||||||
const lon0 = Number(args[4])
|
const lon0 = Number(args[4])
|
||||||
const lat1 = Number(args[5])
|
const lat1 = Number(args[5])
|
||||||
const lon1 = Number(args[6])
|
const lon1 = Number(args[6])
|
||||||
|
const clip = args.indexOf("--clip") >= 0
|
||||||
|
|
||||||
if (isNaN(lat0)) {
|
if (isNaN(lat0)) {
|
||||||
throw "The first number (a latitude) is not a valid number"
|
throw "The first number (a latitude) is not a valid number"
|
||||||
|
@ -570,7 +586,7 @@ export async function main(args: string[]) {
|
||||||
|
|
||||||
const extraFeatures = await downloadExtraData(theme)
|
const extraFeatures = await downloadExtraData(theme)
|
||||||
const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
|
const allFeaturesSource = loadAllTiles(targetdir, tileRange, theme, extraFeatures)
|
||||||
sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor)
|
sliceToTiles(allFeaturesSource, theme, relationTracker, targetdir, generatePointLayersFor, clip)
|
||||||
}
|
}
|
||||||
|
|
||||||
let args = [...process.argv]
|
let args = [...process.argv]
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { describe } from "mocha"
|
||||||
import { expect } from "chai"
|
import { expect } from "chai"
|
||||||
import * as turf from "@turf/turf"
|
import * as turf from "@turf/turf"
|
||||||
import { GeoOperations } from "../../Logic/GeoOperations"
|
import { GeoOperations } from "../../Logic/GeoOperations"
|
||||||
|
import { Feature, LineString, Polygon } from "geojson"
|
||||||
|
|
||||||
describe("GeoOperations", () => {
|
describe("GeoOperations", () => {
|
||||||
describe("calculateOverlap", () => {
|
describe("calculateOverlap", () => {
|
||||||
|
@ -133,4 +134,44 @@ describe("GeoOperations", () => {
|
||||||
expect(overlapsRev).empty
|
expect(overlapsRev).empty
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe("clipWith", () => {
|
||||||
|
it("clipWith should clip linestrings", () => {
|
||||||
|
const bbox: Feature<Polygon> = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {},
|
||||||
|
geometry: {
|
||||||
|
coordinates: [
|
||||||
|
[
|
||||||
|
[3.218560377159008, 51.21600586532159],
|
||||||
|
[3.218560377159008, 51.21499687768525],
|
||||||
|
[3.2207456783268356, 51.21499687768525],
|
||||||
|
[3.2207456783268356, 51.21600586532159],
|
||||||
|
[3.218560377159008, 51.21600586532159],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
type: "Polygon",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const line: Feature<LineString> = {
|
||||||
|
type: "Feature",
|
||||||
|
properties: {},
|
||||||
|
geometry: {
|
||||||
|
coordinates: [
|
||||||
|
[3.218405371672816, 51.21499091846559],
|
||||||
|
[3.2208408127450525, 51.21560173433727],
|
||||||
|
],
|
||||||
|
type: "LineString",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const result = GeoOperations.clipWith(line, bbox)
|
||||||
|
expect(result.length).to.equal(1)
|
||||||
|
expect(result[0].geometry.type).to.eq("LineString")
|
||||||
|
const clippedLine = (<Feature<LineString>>result[0]).geometry.coordinates
|
||||||
|
const expCoordinates = [
|
||||||
|
[3.2185480732975975, 51.21502965337126],
|
||||||
|
[3.2207456783252724, 51.2155808773463],
|
||||||
|
]
|
||||||
|
expect(clippedLine).to.deep.equal(expCoordinates)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue