CLI for Orbis.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

176 lines
4.2 KiB

  1. import yargs from 'yargs';
  2. import { hideBin } from 'yargs/helpers';
  3. import { project, ProjectBounds } from '@theoryofnekomata/orbis-core';
  4. import { writeFile } from 'fs/promises';
  5. import { basename, dirname, resolve } from 'path';
  6. type ProjectArgs = {
  7. input: string,
  8. projection: string,
  9. output?: string,
  10. bounds: ProjectBounds,
  11. width?: number,
  12. height?: number,
  13. padding: [number, number],
  14. country?: string,
  15. };
  16. const coerceNumber = (n?: string) => {
  17. if (typeof n === 'undefined') {
  18. return undefined;
  19. }
  20. const tryWidth = Number(n);
  21. if (Number.isFinite(tryWidth)) {
  22. return tryWidth;
  23. }
  24. return undefined;
  25. };
  26. const coercePadding = (n?: string) => {
  27. if (typeof n !== 'string') {
  28. return [0, 0];
  29. }
  30. const [paddingXString = '0', paddingYString = paddingXString] = n.split(';');
  31. return [
  32. Number(paddingXString),
  33. Number(paddingYString),
  34. ];
  35. };
  36. const coerceBounds = (n?: string): ProjectBounds => {
  37. if (typeof n !== 'string') {
  38. return {
  39. type: 'geojson',
  40. value: {
  41. type: 'Sphere',
  42. },
  43. };
  44. }
  45. const [boundsType, etcBounds] = n.split(':');
  46. if (boundsType === 'geojson') {
  47. const [geometryType, etcArgs] = etcBounds.split(';');
  48. if (geometryType === 'Sphere') {
  49. return {
  50. type: boundsType,
  51. value: {
  52. type: geometryType,
  53. },
  54. };
  55. }
  56. if (geometryType === 'Polygon' || geometryType === 'MultiPolygon') {
  57. return {
  58. type: boundsType,
  59. value: {
  60. type: geometryType,
  61. coordinates: JSON.parse(etcArgs),
  62. },
  63. };
  64. }
  65. return {
  66. type: 'geojson',
  67. value: {
  68. type: 'Sphere',
  69. },
  70. };
  71. }
  72. if (boundsType === 'country') {
  73. const [country] = etcBounds.split(';');
  74. return {
  75. type: boundsType,
  76. value: country,
  77. };
  78. }
  79. return {
  80. type: 'geojson',
  81. value: {
  82. type: 'Sphere',
  83. },
  84. };
  85. };
  86. const main = async (argv: string | readonly string[]) => {
  87. await yargs
  88. .option('interactive', {
  89. alias: 'i',
  90. })
  91. .command({
  92. command: 'project <input> <projection>',
  93. aliases: 'p',
  94. describe: 'Project an equirectangular PNG image to another projection.',
  95. builder: (y) => y
  96. .option('output', {
  97. alias: 'o',
  98. })
  99. .coerce('output', (output) => {
  100. if (!output) {
  101. return null;
  102. }
  103. if (typeof output !== 'string') {
  104. return null;
  105. }
  106. return output;
  107. })
  108. .option('width', {
  109. alias: 'w',
  110. })
  111. .coerce('width', coerceNumber)
  112. .option('height', {
  113. alias: 'h',
  114. })
  115. .coerce('height', coerceNumber)
  116. .option('padding', {
  117. alias: 'p',
  118. default: '0;0',
  119. })
  120. .coerce('padding', coercePadding)
  121. .option('bounds', {
  122. alias: 'b',
  123. default: 'geojson:Sphere',
  124. })
  125. .option('country', {
  126. alias: 'c',
  127. })
  128. .coerce('bounds', coerceBounds),
  129. handler: async (projectArgvRaw) => {
  130. const projectArgv = projectArgvRaw as unknown as ProjectArgs;
  131. const outputPng = await project(projectArgv.input, [projectArgv.projection], {
  132. bounds: projectArgv.bounds,
  133. wrapAround: false,
  134. outputSize: {
  135. width: projectArgv.width,
  136. height: projectArgv.height,
  137. },
  138. outputPadding: {
  139. x: projectArgv.padding[0],
  140. y: projectArgv.padding[1],
  141. },
  142. });
  143. if (!outputPng) {
  144. process.stdout.write('No output created.\n');
  145. return;
  146. }
  147. const outputFilename = projectArgv.output ?? `${basename(projectArgv.input, '.png')}.out.png`;
  148. const outputPath = resolve(
  149. dirname(projectArgv.input),
  150. outputFilename.endsWith('.png') ? outputFilename : `${outputFilename}.png`,
  151. );
  152. await writeFile(outputPath, outputPng);
  153. process.stdout.write(`Created output file: ${outputPath}\n`);
  154. },
  155. })
  156. .demandCommand(1, 'Please specify a command')
  157. .help()
  158. .parse(
  159. hideBin((Array.isArray(argv) ? argv : [argv]) as string[]),
  160. );
  161. };
  162. void main(process.argv);