UNPKG

digipinjs

Version:

A comprehensive TypeScript library for encoding and decoding Indian geographic coordinates into DIGIPIN format (Indian Postal Digital PIN system). Features CLI tools, caching, batch processing, and Express middleware for seamless integration.

548 lines (547 loc) 19.8 kB
#!/usr/bin/env node "use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs_1 = __importDefault(require("fs")); const chalk_1 = __importDefault(require("chalk")); const yargs_1 = __importDefault(require("yargs")); const helpers_1 = require("yargs/helpers"); const core_1 = require("./core"); const batch_1 = require("./batch"); const geo_1 = require("./geo"); const errors_1 = require("./errors"); const util_1 = require("./util"); const watch_1 = require("./watch"); function toDMS(decimal, isLatitude) { const absolute = Math.abs(decimal); const degrees = Math.floor(absolute); const minutesNotTruncated = (absolute - degrees) * 60; const minutes = Math.floor(minutesNotTruncated); const seconds = ((minutesNotTruncated - minutes) * 60).toFixed(2); const direction = isLatitude ? (decimal >= 0 ? 'N' : 'S') : decimal >= 0 ? 'E' : 'W'; return `${degrees}° ${minutes}' ${seconds}" ${direction}`; } function formatCoords(coords, format = 'degrees') { if (format === 'dms') { return `Latitude: ${toDMS(coords.latitude, true)}\nLongitude: ${toDMS(coords.longitude, false)}`; } return `Latitude: ${chalk_1.default.cyan(coords.latitude.toFixed(6))}°\nLongitude: ${chalk_1.default.cyan(coords.longitude.toFixed(6))}°`; } function printJson(payload, pretty = true) { console.log(JSON.stringify(payload, null, pretty ? 2 : 0)); } function handleError(error) { if (error instanceof errors_1.BoundsError) { console.error(chalk_1.default.red('\nError:'), error.message); console.error(chalk_1.default.yellow('Valid latitude range is'), `${core_1.BOUNDS.minLat}° to ${core_1.BOUNDS.maxLat}°`); console.error(chalk_1.default.yellow('Valid longitude range is'), `${core_1.BOUNDS.minLon}° to ${core_1.BOUNDS.maxLon}°`); } else if (error instanceof errors_1.PinFormatError) { console.error(chalk_1.default.red('\nError:'), 'DIGIPIN must be 10 characters long (excluding hyphens)'); } else if (error instanceof errors_1.InvalidCharacterError) { console.error(chalk_1.default.red('\nError:'), `DIGIPIN contains unsupported character "${chalk_1.default.cyan(error.character)}"`); } else if (error instanceof errors_1.DigiPinError) { console.error(chalk_1.default.red('\nError:'), error.message); } else if (error instanceof Error) { console.error(chalk_1.default.red('\nError:'), error.message); } else { console.error(chalk_1.default.red('\nUnknown error'), error); } process.exit(1); } function validateCoordinates(lat, lng) { if (Number.isNaN(lat) || Number.isNaN(lng)) { throw new Error('Invalid coordinates: must be numbers'); } if (lat < core_1.BOUNDS.minLat || lat > core_1.BOUNDS.maxLat) { throw new errors_1.BoundsError(lat, undefined, core_1.BOUNDS); } if (lng < core_1.BOUNDS.minLon || lng > core_1.BOUNDS.maxLon) { throw new errors_1.BoundsError(undefined, lng, core_1.BOUNDS); } } function loadJsonFile(path) { const contents = fs_1.default.readFileSync(path, 'utf8'); return JSON.parse(contents); } const encodeCommand = { command: 'encode', describe: 'Convert latitude/longitude to DIGIPIN', builder: (yargs) => yargs .option('lat', { type: 'number', describe: 'Latitude (2.5° to 38.5°)', }) .option('lng', { type: 'number', describe: 'Longitude (63.5° to 99.5°)', }) .option('verbose', { type: 'boolean', default: false, describe: 'Show detailed information', }) .option('json', { type: 'boolean', default: false, describe: 'Emit JSON output', }) .option('format', { type: 'string', choices: ['degrees', 'dms'], default: 'degrees', describe: 'Coordinate format for verbose output', }) .option('pin-format', { type: 'string', choices: ['hyphenated', 'compact'], default: 'hyphenated', describe: 'DIGIPIN output format', }) .option('round', { type: 'number', describe: 'Round coordinates to the specified decimals before encoding (default: 6)', }) .option('no-round', { type: 'boolean', default: false, describe: 'Disable coordinate rounding', }) .option('cache', { type: 'boolean', default: true, describe: 'Enable encode cache', }) .option('watch', { type: 'boolean', default: false, describe: 'Read coordinates from stdin (one per line)', }) .option('watch-format', { type: 'string', choices: ['json', 'csv'], default: 'json', describe: 'Input format for watch mode', }) .check((argv) => { if (!argv.watch) { if (argv.lat === undefined || argv.lng === undefined) { throw new Error('lat and lng are required unless --watch is enabled'); } } return true; }), handler: (argv) => { try { const pinFormat = (argv.pinFormat ?? 'hyphenated'); const roundTo = argv.noRound ? 'none' : argv.round ?? 6; if (argv.watch) { const watchFormat = (argv.watchFormat ?? 'json'); (0, watch_1.watchEncodeStream)(process.stdin, { format: pinFormat, roundTo, useCache: argv.cache ?? true, inputFormat: watchFormat, onResult: ({ lat, lng, pin }) => { if (argv.json) { printJson({ lat, lng, pin, format: pinFormat }); } else { console.log(chalk_1.default.cyan(pin)); } }, onError: (error, raw) => { console.error(chalk_1.default.red('Failed to encode input line:'), raw); console.error(error.message); }, }); return; } const lat = argv.lat; const lng = argv.lng; validateCoordinates(lat, lng); const pin = (0, core_1.getDigiPin)(lat, lng, { format: pinFormat, roundTo, useCache: argv.cache ?? true, }); if (argv.json) { printJson({ latitude: lat, longitude: lng, pin, format: pinFormat, }); return; } if (argv.verbose) { console.log(chalk_1.default.green('\nInput Coordinates:')); console.log(formatCoords({ latitude: lat, longitude: lng }, argv.format)); console.log(chalk_1.default.green('\nGenerated DIGIPIN:'), chalk_1.default.cyan(pin)); console.log(chalk_1.default.green('\nDIGIPIN Format:'), 'XXX-XXX-XXXX'); console.log(chalk_1.default.green('Valid Characters:'), 'F,C,9,8,J,3,2,7,K,4,5,6,L,M,P,T'); } else { console.log(chalk_1.default.cyan('\nDIGIPIN:'), pin); } } catch (error) { handleError(error); } }, }; const decodeCommand = { command: 'decode', describe: 'Convert DIGIPIN to latitude/longitude', builder: (yargs) => yargs .option('pin', { type: 'string', describe: 'DIGIPIN to decode (format: XXX-XXX-XXXX)', }) .option('verbose', { type: 'boolean', default: false, describe: 'Show detailed information', }) .option('json', { type: 'boolean', default: false, describe: 'Emit JSON output', }) .option('format', { type: 'string', choices: ['degrees', 'dms'], default: 'degrees', describe: 'Coordinate format for output', }) .option('cache', { type: 'boolean', default: true, describe: 'Enable decode cache', }) .option('watch', { type: 'boolean', default: false, describe: 'Read DIGIPINs from stdin (one per line)', }) .check((argv) => { if (!argv.watch && !argv.pin) { throw new Error('pin is required unless --watch is enabled'); } return true; }), handler: (argv) => { try { if (argv.watch) { (0, watch_1.watchDecodeStream)(process.stdin, { useCache: argv.cache ?? true, onResult: ({ pin, latitude, longitude }) => { if (argv.json) { printJson({ pin, latitude, longitude }); } else { console.log(formatCoords({ latitude, longitude }, (argv.format ?? 'degrees'))); } }, onError: (error, raw) => { console.error(chalk_1.default.red('Failed to decode input line:'), raw); console.error(error.message); }, }); return; } const pin = argv.pin; (0, util_1.normalizeDigiPin)(pin); const coords = (0, core_1.getLatLngFromDigiPin)(pin, { useCache: argv.cache ?? true, }); if (argv.json) { printJson({ pin, ...coords }); return; } if (argv.verbose) { console.log(chalk_1.default.green('\nInput DIGIPIN:'), chalk_1.default.cyan(pin)); console.log(chalk_1.default.green('DIGIPIN Format:'), 'XXX-XXX-XXXX'); console.log(chalk_1.default.green('Valid Characters:'), 'F,C,9,8,J,3,2,7,K,4,5,6,L,M,P,T'); console.log(chalk_1.default.green('\nDecoded Coordinates:')); console.log(formatCoords(coords, argv.format)); console.log(chalk_1.default.green('\nCoordinate Bounds:')); console.log(chalk_1.default.yellow('Latitude:'), `${core_1.BOUNDS.minLat}° to ${core_1.BOUNDS.maxLat}°`); console.log(chalk_1.default.yellow('Longitude:'), `${core_1.BOUNDS.minLon}° to ${core_1.BOUNDS.maxLon}°`); } else { console.log(chalk_1.default.green('\nCoordinates:')); console.log(formatCoords(coords, argv.format)); } } catch (error) { handleError(error); } }, }; const batchEncodeCommand = { command: 'batch-encode <file>', describe: 'Batch encode coordinates from a JSON file', builder: (yargs) => yargs .positional('file', { type: 'string', describe: 'Path to JSON file (array of { lat, lng })', demandOption: true, }) .option('json', { type: 'boolean', default: false, describe: 'Emit JSON output', }) .option('pin-format', { type: 'string', choices: ['hyphenated', 'compact'], default: 'hyphenated', describe: 'DIGIPIN output format', }) .option('round', { type: 'number', describe: 'Round coordinates to the specified decimals before encoding (default: 6)', }) .option('no-round', { type: 'boolean', default: false, describe: 'Disable coordinate rounding', }) .option('cache', { type: 'boolean', default: true, describe: 'Enable encode cache', }), handler: (argv) => { try { const data = loadJsonFile(argv.file); if (!Array.isArray(data)) { throw new Error('Batch encode file must contain an array of coordinates'); } const pinFormat = (argv.pinFormat ?? 'hyphenated'); const roundTo = argv.noRound ? 'none' : argv.round ?? 6; const encodeOptions = { format: pinFormat, roundTo, useCache: argv.cache ?? true, }; const pins = (0, batch_1.batchEncode)(data.map((entry) => { if (typeof entry !== 'object' || entry === null) { throw new Error('Each coordinate must be an object with lat and lng'); } const lat = entry.lat ?? entry.latitude; const lng = entry.lng ?? entry.longitude; if (typeof lat !== 'number' || typeof lng !== 'number') { throw new Error('Coordinate entries must contain numeric lat/lng'); } validateCoordinates(lat, lng); return { lat, lng }; }), encodeOptions); if (argv.json) { printJson(pins); } else { pins.forEach((pin) => console.log(pin)); } } catch (error) { handleError(error); } }, }; const batchDecodeCommand = { command: 'batch-decode <file>', describe: 'Batch decode DIGIPINs from a JSON file', builder: (yargs) => yargs .positional('file', { type: 'string', describe: 'Path to JSON file (array of DIGIPIN strings)', demandOption: true, }) .option('json', { type: 'boolean', default: false, describe: 'Emit JSON output', }) .option('cache', { type: 'boolean', default: true, describe: 'Enable decode cache', }), handler: (argv) => { try { const data = loadJsonFile(argv.file); if (!Array.isArray(data)) { throw new Error('Batch decode file must contain an array of DIGIPIN strings'); } const pins = data.map((entry) => { if (typeof entry !== 'string') { throw new Error('Batch decode entries must be DIGIPIN strings'); } (0, util_1.normalizeDigiPin)(entry); return entry; }); const coords = (0, batch_1.batchDecode)(pins, { useCache: argv.cache ?? true, }); if (argv.json) { printJson(coords); } else { coords.forEach((coord, index) => { console.log(chalk_1.default.green(`\n${pins[index]}`)); console.log(formatCoords(coord)); }); } } catch (error) { handleError(error); } }, }; const distanceCommand = { command: 'distance', describe: 'Calculate distance (in meters) between two DIGIPINs', builder: (yargs) => yargs .option('from', { type: 'string', describe: 'Start DIGIPIN', demandOption: true, }) .option('to', { type: 'string', describe: 'End DIGIPIN', demandOption: true, }) .option('precise', { type: 'boolean', default: false, describe: 'Use Vincenty algorithm for higher precision', }) .option('accuracy', { type: 'number', default: 1, describe: 'Distance accuracy in meters (passed to geolib)', }) .option('json', { type: 'boolean', default: false, describe: 'Emit JSON output', }), handler: (argv) => { try { (0, util_1.normalizeDigiPin)(argv.from); (0, util_1.normalizeDigiPin)(argv.to); const fn = argv.precise ? geo_1.getPreciseDistance : geo_1.getDistance; const distance = fn(argv.from, argv.to, argv.accuracy ?? 1); if (argv.json) { printJson({ from: argv.from, to: argv.to, distance, precise: argv.precise ?? false, accuracy: argv.accuracy ?? 1, }); } else { console.log(chalk_1.default.green(`Distance between ${chalk_1.default.cyan(argv.from)} and ${chalk_1.default.cyan(argv.to)}: ${chalk_1.default.yellow(`${distance} m`)}`)); } } catch (error) { handleError(error); } }, }; const nearestCommand = { command: 'nearest', describe: 'Find the nearest DIGIPIN from a list relative to a reference pin', builder: (yargs) => yargs .option('reference', { type: 'string', describe: 'Reference DIGIPIN', demandOption: true, }) .option('file', { type: 'string', describe: 'JSON file containing an array of DIGIPINs', }) .option('pins', { type: 'string', describe: 'Comma separated list of DIGIPINs', }) .option('accuracy', { type: 'number', default: 1, describe: 'Distance accuracy in meters (passed to geolib)', }) .option('json', { type: 'boolean', default: false, describe: 'Emit JSON output', }) .check((argv) => { if (!argv.file && !argv.pins) { throw new Error('You must provide either --file or --pins'); } return true; }), handler: (argv) => { try { (0, util_1.normalizeDigiPin)(argv.reference); let pinList = []; if (argv.file) { const data = loadJsonFile(argv.file); if (!Array.isArray(data) || data.some((entry) => typeof entry !== 'string')) { throw new Error('Nearest pins file must be an array of DIGIPIN strings'); } pinList = data; } else if (argv.pins) { pinList = argv.pins.split(',').map((pin) => pin.trim()); } pinList.forEach((pin) => (0, util_1.normalizeDigiPin)(pin)); const ordered = (0, geo_1.orderByDistance)(argv.reference, pinList, { accuracy: argv.accuracy ?? 1, }); const nearest = ordered[0]; if (!nearest) { throw new Error('No pins available to compare'); } const distance = (0, geo_1.getDistance)(argv.reference, nearest, argv.accuracy ?? 1); if (argv.json) { printJson({ reference: argv.reference, nearest, distance, }); } else { console.log(chalk_1.default.green(`Nearest to ${chalk_1.default.cyan(argv.reference)} is ${chalk_1.default.cyan(nearest)} at ${chalk_1.default.yellow(`${distance} m`)}`)); } } catch (error) { handleError(error); } }, }; (0, yargs_1.default)((0, helpers_1.hideBin)(process.argv)) .scriptName('digipin-cli') .command(encodeCommand) .command(decodeCommand) .command(batchEncodeCommand) .command(batchDecodeCommand) .command(distanceCommand) .command(nearestCommand) .demandCommand(1, chalk_1.default.yellow('Please specify a command')) .strict() .help() .version() .epilog(chalk_1.default.gray('For more information, visit: https://github.com/rajatguptaa/digipinjs')) .argv;