2025-07-31 01:03:54 +02:00
import Script from "./Script"
import { Tiles } from "../src/Models/TileRange"
import { OfflineBasemapManager } from "../src/service-worker/OfflineBasemapManager"
import { spawn } from "child_process"
2025-07-31 11:57:23 +02:00
import { existsSync } from "fs"
2025-07-31 12:18:57 +02:00
import { Utils } from "../src/Utils"
2025-07-31 01:03:54 +02:00
class GeneratePmTilesExtracts extends Script {
2025-07-31 01:11:12 +02:00
private targetDir : string
2025-07-31 12:04:20 +02:00
private skipped : number = 0
2025-07-31 01:03:54 +02:00
constructor ( ) {
2025-08-13 23:06:38 +02:00
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"
)
2025-07-31 01:03:54 +02:00
}
2025-07-31 01:11:12 +02:00
startProcess ( script : string ) : Promise < void > {
return new Promise ( ( resolve , reject ) = > {
2025-08-13 23:06:38 +02:00
const child = spawn ( "/data/pmtiles" , script . split ( " " ) , {
stdio : "ignore" ,
cwd : this.targetDir ,
} )
2025-07-31 01:11:12 +02:00
child . on ( "close" , ( code ) = > {
if ( code === 0 ) resolve ( )
else reject ( new Error ( ` Process exited with code ${ code } ` ) )
} )
child . on ( "error" , reject )
} )
}
2025-07-31 01:03:54 +02:00
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 = ""
2025-08-13 23:06:38 +02:00
if ( maxzoom !== undefined ) {
maxzoomflag = " --maxzoom=" + maxzoom
2025-07-31 01:03:54 +02:00
}
2025-08-13 23:06:38 +02:00
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 ) } `
)
2025-07-31 01:03:54 +02:00
}
2025-08-13 23:06:38 +02:00
private * generateColumnIfNeeded (
z : number ,
x : number ,
boundary : number ,
maxzoom? : number
) : Generator < Promise < void > > {
2025-07-31 13:43:21 +02:00
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
}
2025-08-13 23:06:38 +02:00
console . log (
"Starting column" ,
x ,
"at zoom" ,
z ,
"as" ,
this . targetDir + "/" + lastFileForColumn ,
"does not exist"
)
2025-07-31 13:43:21 +02:00
for ( let y = 0 ; y < boundary ; y ++ ) {
yield this . generateArchive ( z , x , y , maxzoom )
}
}
2025-08-13 23:06:38 +02:00
private * generateField ( z : number , maxzoom? : number ) : Generator < Promise < void > > {
2025-07-31 01:19:45 +02:00
const boundary = 2 << ( z - 1 )
2025-08-13 23:06:38 +02:00
2025-07-31 01:03:54 +02:00
for ( let x = 0 ; x < boundary ; x ++ ) {
2025-07-31 13:48:31 +02:00
for ( const promise of this . generateColumnIfNeeded ( z , x , boundary , maxzoom ) ) {
yield promise
}
2025-07-31 01:03:54 +02:00
}
}
2025-07-31 11:57:23 +02:00
private getFilename ( z : number , x : number , y : number ) {
2025-07-31 12:12:56 +02:00
return ` ${ z } - ${ x } - ${ y } .pmtiles `
2025-07-31 11:57:23 +02:00
}
2025-08-13 23:06:38 +02:00
private * generateAll ( ) : Generator < Promise < void > > {
2025-07-31 01:03:54 +02:00
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
}
2025-07-31 01:11:12 +02:00
async main ( args : string [ ] ) : Promise < void > {
this . targetDir = args [ 0 ]
if ( ! this . targetDir ) {
console . log ( "Please specify a target directory" )
return
}
2025-07-31 11:57:23 +02:00
let estimate = 0
for ( const key in OfflineBasemapManager . zoomelevels ) {
const z : number = Number ( key )
2025-08-01 02:05:17 +02:00
const boundary = 2 << z
2025-07-31 11:57:23 +02:00
estimate += boundary * boundary
}
2025-07-31 01:11:45 +02:00
console . log ( "Target dir is:" , this . targetDir )
2025-07-31 01:22:36 +02:00
const numberOfThreads = 512
2025-07-31 01:03:54 +02:00
const generator = this . generateAll ( )
let batch : Promise < void > [ ] = [ ]
2025-07-31 01:22:36 +02:00
let done = 0
2025-07-31 12:18:57 +02:00
const startDate = new Date ( )
2025-07-31 01:03:54 +02:00
do {
batch = this . createBatch ( generator , numberOfThreads )
await Promise . all ( batch )
2025-07-31 01:22:36 +02:00
done += batch . length
2025-07-31 12:18:57 +02:00
const now = new Date ( )
const timeElapsed = ( now . getTime ( ) - startDate . getTime ( ) ) / 1000
2025-08-13 23:06:38 +02:00
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
) } `
)
2025-07-31 01:03:54 +02:00
} while ( batch . length > 0 )
}
}
new GeneratePmTilesExtracts ( ) . run ( )