import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; import { project, ProjectBounds } from '@theoryofnekomata/orbis-core'; import { writeFile } from 'fs/promises'; import { basename, dirname, resolve } from 'path'; type ProjectArgs = { input: string, projection: string, output?: string, bounds: ProjectBounds, width?: number, height?: number, padding: [number, number], country?: string, }; const coerceNumber = (n?: string) => { if (typeof n === 'undefined') { return undefined; } const tryWidth = Number(n); if (Number.isFinite(tryWidth)) { return tryWidth; } return undefined; }; const coercePadding = (n?: string) => { if (typeof n !== 'string') { return [0, 0]; } const [paddingXString = '0', paddingYString = paddingXString] = n.split(';'); return [ Number(paddingXString), Number(paddingYString), ]; }; const coerceBounds = (n?: string): ProjectBounds => { if (typeof n !== 'string') { return { type: 'geojson', value: { type: 'Sphere', }, }; } const [boundsType, etcBounds] = n.split(':'); if (boundsType === 'geojson') { const [geometryType, etcArgs] = etcBounds.split(';'); if (geometryType === 'Sphere') { return { type: boundsType, value: { type: geometryType, }, }; } if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') { return { type: boundsType, value: { type: geometryType, coordinates: JSON.parse(etcArgs), }, }; } return { type: 'geojson', value: { type: 'Sphere', }, }; } if (boundsType === 'country') { const [country] = etcBounds.split(';'); return { type: boundsType, value: country, }; } return { type: 'geojson', value: { type: 'Sphere', }, }; }; const main = async (argv: string | readonly string[]) => { await yargs .option('interactive', { alias: 'i', }) .command({ command: 'project ', aliases: 'p', describe: 'Project an equirectangular PNG image to another projection.', builder: (y) => y .option('output', { alias: 'o', }) .coerce('output', (output) => { if (!output) { return null; } if (typeof output !== 'string') { return null; } return output; }) .option('width', { alias: 'w', }) .coerce('width', coerceNumber) .option('height', { alias: 'h', }) .coerce('height', coerceNumber) .option('padding', { alias: 'p', default: '0;0', }) .coerce('padding', coercePadding) .option('bounds', { alias: 'b', default: 'geojson:Sphere', }) .option('country', { alias: 'c', }) .coerce('bounds', coerceBounds), handler: async (projectArgvRaw) => { const projectArgv = projectArgvRaw as unknown as ProjectArgs; const outputPng = await project(projectArgv.input, [projectArgv.projection], { bounds: projectArgv.bounds, wrapAround: false, outputSize: { width: projectArgv.width, height: projectArgv.height, }, outputPadding: { x: projectArgv.padding[0], y: projectArgv.padding[1], }, }); if (!outputPng) { process.stdout.write('No output created.\n'); return; } const outputFilename = projectArgv.output ?? `${basename(projectArgv.input, '.png')}.out.png`; const outputPath = resolve( dirname(projectArgv.input), outputFilename.endsWith('.png') ? outputFilename : `${outputFilename}.png`, ); await writeFile(outputPath, outputPng); process.stdout.write(`Created output file: ${outputPath}\n`); }, }) .demandCommand(1, 'Please specify a command') .help() .parse( hideBin((Array.isArray(argv) ? argv : [argv]) as string[]), ); }; void main(process.argv);