forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			221 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			221 lines
		
	
	
	
		
			7.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
| 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<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
 | |
|     }
 | |
| 
 | |
|     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)
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     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")
 | |
|         }
 | |
|         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()
 |