mapcomplete/scripts/slice.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

222 lines
7.1 KiB
TypeScript
Raw Permalink Normal View History

2021-10-27 01:18:05 +02:00
import * as fs from "fs"
import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource"
import * as readline from "readline"
import ScriptUtils from "./ScriptUtils"
2022-01-16 01:59:06 +01:00
import { Utils } from "../Utils"
import Script from "./Script"
import { BBox } from "../Logic/BBox"
import { GeoOperations } from "../Logic/GeoOperations"
2023-03-28 05:13:48 +02:00
import { Tiles } from "../Models/TileRange"
import { Feature } from "geojson"
2021-10-27 01:18:05 +02:00
2021-12-09 17:56:37 +01:00
/**
2022-01-26 21:40:38 +01:00
* This script slices a big newline-delimeted geojson file into tiled geojson
2021-12-09 17:56:37 +01:00
* It was used to convert the CRAB-data into geojson tiles
*/
class Slice extends Script {
constructor() {
super("Break data into tiles")
}
2021-10-27 01:18:05 +02:00
async readFeaturesFromLineDelimitedJsonFile(inputFile: string): Promise<any[]> {
const fileStream = fs.createReadStream(inputFile)
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
})
// Note: we use the crlfDelay option to recognize all instances of CR LF
// ('\r\n') in input.txt as a single line break.
const allFeatures: any[] = []
// @ts-ignore
for await (const line of rl) {
try {
allFeatures.push(JSON.parse(line))
} catch (e) {
console.error("Could not parse", line)
break
}
if (allFeatures.length % 10000 === 0) {
ScriptUtils.erasableLog("Loaded ", allFeatures.length, "features up till now")
}
}
return allFeatures
2021-10-27 01:18:05 +02:00
}
async readGeojsonLineByLine(inputFile: string): Promise<any[]> {
const fileStream = fs.createReadStream(inputFile)
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity,
})
// Note: we use the crlfDelay option to recognize all instances of CR LF
// ('\r\n') in input.txt as a single line break.
const allFeatures: any[] = []
let featuresSeen = false
// @ts-ignore
for await (let line: string of rl) {
if (!featuresSeen && line.startsWith('"features":')) {
featuresSeen = true
continue
}
if (!featuresSeen) {
continue
}
if (line.endsWith(",")) {
line = line.substring(0, line.length - 1)
}
try {
allFeatures.push(JSON.parse(line))
} catch (e) {
console.error("Could not parse", line)
break
}
if (allFeatures.length % 10000 === 0) {
ScriptUtils.erasableLog("Loaded ", allFeatures.length, "features up till now")
}
}
return allFeatures
}
async readFeaturesFromGeoJson(inputFile: string): Promise<any[]> {
try {
return JSON.parse(fs.readFileSync(inputFile, { encoding: "utf-8" })).features
} catch (e) {
// We retry, but with a line-by-line approach
return await this.readGeojsonLineByLine(inputFile)
2021-10-27 01:18:05 +02:00
}
}
2023-03-28 05:13:48 +02:00
private handleTileData(
features: Feature[],
tileIndex: number,
outputDirectory: string,
doSlice: boolean,
handled: number,
maxNumberOfTiles: number
) {
const [z, x, y] = Tiles.tile_from_index(tileIndex)
const path = `${outputDirectory}/tile_${z}_${x}_${y}.geojson`
const box = BBox.fromTileIndex(tileIndex)
if (doSlice) {
features = Utils.NoNull(
features.map((f) => {
const bbox = box.asGeoJson({})
const properties = {
...f.properties,
id: (f.properties?.id ?? "") + "_" + z + "_" + x + "_" + y,
}
if (GeoOperations.completelyWithin(bbox, <any>f)) {
bbox.properties = properties
return bbox
}
const intersection = GeoOperations.intersect(f, box.asGeoJson({}))
if (intersection) {
intersection.properties = properties
}
return intersection
})
)
}
features.forEach((f) => {
delete f.bbox
})
if (features.length === 0) {
ScriptUtils.erasableLog(
handled + "/" + maxNumberOfTiles,
"Not writing ",
path,
": no features"
)
return
}
fs.writeFileSync(
path,
JSON.stringify(
{
type: "FeatureCollection",
features: features,
},
null,
" "
)
)
ScriptUtils.erasableLog(
handled + "/" + maxNumberOfTiles,
"Written ",
path,
"which has ",
features.length,
"features"
)
}
async main(args: string[]) {
console.log("GeoJSON slicer")
if (args.length < 3) {
console.log(
"USAGE: <input-file.geojson> <target-zoom-level> <output-directory> [--clip]"
)
return
}
const inputFile = args[0]
const zoomlevel = Number(args[1])
const outputDirectory = args[2]
const doSlice = args[3] === "--clip"
if (!fs.existsSync(outputDirectory)) {
fs.mkdirSync(outputDirectory)
console.log("Directory created")
2022-01-16 01:59:06 +01:00
}
console.log("Using directory ", outputDirectory)
let allFeatures: any[]
if (inputFile.endsWith(".geojson")) {
console.log("Detected geojson")
allFeatures = await this.readFeaturesFromGeoJson(inputFile)
} else {
console.log("Loading as newline-delimited features")
allFeatures = await this.readFeaturesFromLineDelimitedJsonFile(inputFile)
2021-10-27 01:18:05 +02:00
}
allFeatures = Utils.NoNull(allFeatures)
console.log("Loaded all", allFeatures.length, "points")
const keysToRemove = ["STRAATNMID", "GEMEENTE", "POSTCODE"]
for (const f of allFeatures) {
if (f.properties === null) {
console.log("Got a feature without properties!", f)
continue
}
for (const keyToRm of keysToRemove) {
delete f.properties[keyToRm]
}
delete f.bbox
}
const maxNumberOfTiles = Math.pow(2, zoomlevel) * Math.pow(2, zoomlevel)
let handled = 0
2023-03-28 05:13:48 +02:00
StaticFeatureSource.fromGeojson(allFeatures).features.addCallbackAndRun((feats) => {
GeoOperations.slice(zoomlevel, feats).forEach((tileData, tileIndex) => {
handled = handled + 1
2023-03-28 05:13:48 +02:00
this.handleTileData(
tileData,
tileIndex,
outputDirectory,
doSlice,
handled,
maxNumberOfTiles
)
2023-03-28 05:13:48 +02:00
})
})
}
2021-10-27 01:18:05 +02:00
}
new Slice().run()