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