forked from MapComplete/MapComplete
		
	
		
			
				
	
	
		
			155 lines
		
	
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			155 lines
		
	
	
	
		
			5.1 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import Script from "./Script"
 | 
						|
import { Tiles } from "../src/Models/TileRange"
 | 
						|
import { OfflineBasemapManager } from "../src/service-worker/OfflineBasemapManager"
 | 
						|
 | 
						|
import { spawn } from "child_process"
 | 
						|
import { existsSync } from "fs"
 | 
						|
import { Utils } from "../src/Utils"
 | 
						|
 | 
						|
class GeneratePmTilesExtracts extends Script {
 | 
						|
    private targetDir: string
 | 
						|
    private skipped: number = 0
 | 
						|
    constructor() {
 | 
						|
        super(
 | 
						|
            "Generates many pmtiles-archive from planet-latest.pmtiles. Must be started from the directory where planet-latest.pmtiles resides, archives will be created next to it"
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    startProcess(script: string): Promise<void> {
 | 
						|
        return new Promise((resolve, reject) => {
 | 
						|
            const child = spawn("/data/pmtiles", script.split(" "), {
 | 
						|
                stdio: "ignore",
 | 
						|
                cwd: this.targetDir,
 | 
						|
            })
 | 
						|
 | 
						|
            child.on("close", (code) => {
 | 
						|
                if (code === 0) resolve()
 | 
						|
                else reject(new Error(`Process exited with code ${code}`))
 | 
						|
            })
 | 
						|
 | 
						|
            child.on("error", reject)
 | 
						|
        })
 | 
						|
    }
 | 
						|
 | 
						|
    private generateArchive(z: number, x: number, y: number, maxzoom?: number): Promise<void> {
 | 
						|
        const [[max_lat, min_lon], [min_lat, max_lon]] = Tiles.tile_bounds(z, x, y)
 | 
						|
        let maxzoomflag = ""
 | 
						|
        if (maxzoom !== undefined) {
 | 
						|
            maxzoomflag = " --maxzoom=" + maxzoom
 | 
						|
        }
 | 
						|
        return this.startProcess(
 | 
						|
            `extract planet-latest.pmtiles --download-threads=1 --minzoom=${z}${maxzoomflag} --bbox=${[
 | 
						|
                min_lon,
 | 
						|
                min_lat + 0.0001,
 | 
						|
                max_lon,
 | 
						|
                max_lat,
 | 
						|
            ].join(",")} ${this.getFilename(z, x, y)}`
 | 
						|
        )
 | 
						|
    }
 | 
						|
 | 
						|
    private *generateColumnIfNeeded(
 | 
						|
        z: number,
 | 
						|
        x: number,
 | 
						|
        boundary: number,
 | 
						|
        maxzoom?: number
 | 
						|
    ): Generator<Promise<void>> {
 | 
						|
        const lastFileForColumn = this.getFilename(z, x, boundary - 1)
 | 
						|
        if (existsSync(this.targetDir + "/" + lastFileForColumn)) {
 | 
						|
            // Skip this column, already exists
 | 
						|
            console.log("Skipping column ", x, "at zoom", z)
 | 
						|
            this.skipped += boundary
 | 
						|
            return
 | 
						|
        }
 | 
						|
        console.log(
 | 
						|
            "Starting column",
 | 
						|
            x,
 | 
						|
            "at zoom",
 | 
						|
            z,
 | 
						|
            "as",
 | 
						|
            this.targetDir + "/" + lastFileForColumn,
 | 
						|
            "does not exist"
 | 
						|
        )
 | 
						|
 | 
						|
        for (let y = 0; y < boundary; y++) {
 | 
						|
            yield this.generateArchive(z, x, y, maxzoom)
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private *generateField(z: number, maxzoom?: number): Generator<Promise<void>> {
 | 
						|
        const boundary = 2 << (z - 1)
 | 
						|
 | 
						|
        for (let x = 0; x < boundary; x++) {
 | 
						|
            for (const promise of this.generateColumnIfNeeded(z, x, boundary, maxzoom)) {
 | 
						|
                yield promise
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    private getFilename(z: number, x: number, y: number) {
 | 
						|
        return `${z}-${x}-${y}.pmtiles`
 | 
						|
    }
 | 
						|
 | 
						|
    private *generateAll(): Generator<Promise<void>> {
 | 
						|
        const zoomlevels: Record<number, number> = OfflineBasemapManager.zoomelevels
 | 
						|
        for (const key in zoomlevels) {
 | 
						|
            const minzoom: number = Number(key)
 | 
						|
            const maxzoom: number | undefined = zoomlevels[key]
 | 
						|
            for (const promise of this.generateField(minzoom, maxzoom)) {
 | 
						|
                yield promise
 | 
						|
            }
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    createBatch<T>(generator: Generator<T>, length: number): T[] {
 | 
						|
        const batch = []
 | 
						|
        do {
 | 
						|
            const next = generator.next()
 | 
						|
            if (next.done) {
 | 
						|
                return batch
 | 
						|
            }
 | 
						|
            batch.push(next.value)
 | 
						|
        } while (batch.length < length)
 | 
						|
        return batch
 | 
						|
    }
 | 
						|
 | 
						|
    async main(args: string[]): Promise<void> {
 | 
						|
        this.targetDir = args[0]
 | 
						|
        if (!this.targetDir) {
 | 
						|
            console.log("Please specify a target directory")
 | 
						|
            return
 | 
						|
        }
 | 
						|
        let estimate = 0
 | 
						|
        for (const key in OfflineBasemapManager.zoomelevels) {
 | 
						|
            const z: number = Number(key)
 | 
						|
            const boundary = 2 << z
 | 
						|
            estimate += boundary * boundary
 | 
						|
        }
 | 
						|
        console.log("Target dir is:", this.targetDir)
 | 
						|
        const numberOfThreads = 512
 | 
						|
        const generator = this.generateAll()
 | 
						|
        let batch: Promise<void>[] = []
 | 
						|
        let done = 0
 | 
						|
        const startDate = new Date()
 | 
						|
        do {
 | 
						|
            batch = this.createBatch(generator, numberOfThreads)
 | 
						|
            await Promise.all(batch)
 | 
						|
            done += batch.length
 | 
						|
            const now = new Date()
 | 
						|
            const timeElapsed = (now.getTime() - startDate.getTime()) / 1000
 | 
						|
            const speed = ("" + done / timeElapsed).substring(0, 5)
 | 
						|
            const perc = ("" + (100 * (done + this.skipped)) / estimate).substring(0, 5)
 | 
						|
            const etaSecond = Math.floor(((estimate - done - this.skipped) * timeElapsed) / done)
 | 
						|
            console.log(
 | 
						|
                "Completed",
 | 
						|
                numberOfThreads,
 | 
						|
                `processes;  ${
 | 
						|
                    done + this.skipped
 | 
						|
                } / ${estimate}, ${perc}%, ${speed} tile/second, ETA: ${Utils.toHumanTime(
 | 
						|
                    etaSecond
 | 
						|
                )}`
 | 
						|
            )
 | 
						|
        } while (batch.length > 0)
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
new GeneratePmTilesExtracts().run()
 |