MapComplete/scripts/generatePmTilesExtracts.ts

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()