import * as fs from "fs" import StaticFeatureSource from "../Logic/FeatureSource/Sources/StaticFeatureSource" import * as readline from "readline" import ScriptUtils from "./ScriptUtils" import { Utils } from "../Utils" import Script from "./Script" import { BBox } from "../Logic/BBox" import { GeoOperations } from "../Logic/GeoOperations" import { Tiles } from "../Models/TileRange" import { Feature } from "geojson" /** * This script slices a big newline-delimeted geojson file into tiled geojson * It was used to convert the CRAB-data into geojson tiles */ class Slice extends Script { constructor() { super("Break data into tiles") } async readFeaturesFromLineDelimitedJsonFile(inputFile: string): Promise { 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 } async readGeojsonLineByLine(inputFile: string): Promise { 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 { 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) } } 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, 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: [--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") } 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) } 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 StaticFeatureSource.fromGeojson(allFeatures).features.addCallbackAndRun((feats) => { GeoOperations.slice(zoomlevel, feats).forEach((tileData, tileIndex) => { handled = handled + 1 this.handleTileData( tileData, tileIndex, outputDirectory, doSlice, handled, maxNumberOfTiles ) }) }) } } new Slice().run()