Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/routes/digipin.routes.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,38 @@
const express = require('express');
const router = express.Router();
const { getDigiPin, getLatLngFromDigiPin } = require('../digipin');
const { getCoordinatesFromPayload } = require('../utils/coordinatesUtils');
const ENCODE_EXAMPLES = 'coordinates="12.903016468272682,77.60606960542954"\nlatitude="12°54\'\'10.3\"N", longitude="77°36\'\'22.3\"E"';

router.post('/encode', (req, res) => { const { latitude, longitude } = req.body; try { const code = getDigiPin(latitude, longitude); res.json({ digipin: code }); } catch (e) { res.status(400).json({ error: e.message }); } });
router.post('/encode', (req, res) => {
try {
const { latitude, longitude } = getCoordinatesFromPayload(req.body);
const code = getDigiPin(latitude, longitude);
res.json({ digipin: code, input: { latitude, longitude } });
} catch (e) {
const msg = e.message || 'Invalid input';
if (msg.includes('Provide "coordinates"')) {
return res.status(400).json({ error: msg, examples: ENCODE_EXAMPLES });
}
res.status(400).json({ error: msg });
}
});

router.post('/decode', (req, res) => { const { digipin } = req.body; try { const coords = getLatLngFromDigiPin(digipin); res.json(coords); } catch (e) { res.status(400).json({ error: e.message }); } });

router.get('/encode', (req, res) => {
const { latitude, longitude } = req.query;
try {
const code = getDigiPin(parseFloat(latitude), parseFloat(longitude));
res.json({ digipin: code });
} catch (e) {
res.status(400).json({ error: e.message });
router.get('/encode', (req, res) => {
try {
const { latitude, longitude } = getCoordinatesFromPayload(req.query);
const code = getDigiPin(latitude, longitude);
res.json({ digipin: code, input: { latitude, longitude } });
} catch (e) {
const msg = e.message || 'Invalid input';
if (msg.includes('Provide "coordinates"')) {
return res.status(400).json({ error: msg, examples: ENCODE_EXAMPLES });
}
});
res.status(400).json({ error: msg });
}
});

router.get('/decode', (req, res) => {
const { digipin } = req.query;
Expand Down
120 changes: 120 additions & 0 deletions src/utils/coordinatesUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
function parseDMS(dmsString) {
if (!dmsString || typeof dmsString !== 'string') {
throw new Error('Invalid DMS string provided');
}

const normalized = dmsString.trim().replace(/\s+/g, ' ');

const dmsRegex = /^(\d+(?:\.\d+)?)°\s*(\d+(?:\.\d+)?)['′]\s*(\d+(?:\.\d+)?)((?:"|″|''))\s*([NSEW])$/i;
const match = normalized.match(dmsRegex);

if (!match) {
throw new Error('Invalid DMS format. Examples: 12°54\'10.3"N or 12°54\'\'10.3"N');
}

const [, degrees, minutes, seconds, , direction] = match;

let decimal = parseFloat(degrees) + parseFloat(minutes) / 60 + parseFloat(seconds) / 3600;

if (direction.toUpperCase() === 'S' || direction.toUpperCase() === 'W') {
decimal = -decimal;
}

return decimal;
}

function parseCoordinate(coordString) {
if (typeof coordString === 'number') {
return coordString;
}

if (typeof coordString !== 'string') {
throw new Error('Coordinate must be a number or string');
}

// Check if it's DMS format (contains degree symbol)
if (coordString.includes('°')) {
return parseDMS(coordString);
}

// Try to parse as decimal
const decimal = parseFloat(coordString);
if (isNaN(decimal)) {
throw new Error('Invalid coordinate format');
}

return decimal;
}

function parseCoordinatePair(lat, lng) {
try {
const latitude = parseCoordinate(lat);
const longitude = parseCoordinate(lng);

return { latitude, longitude };
} catch (error) {
throw new Error(`Coordinate parsing error: ${error.message}`);
}
}

function parseGoogleMapsCoords(coordString) {
if (!coordString || typeof coordString !== 'string') {
throw new Error('Invalid Google Maps coordinate string');
}

const parts = coordString.split(',').map(part => part.trim());

if (parts.length !== 2) {
throw new Error('Google Maps format should contain exactly two coordinates separated by comma');
}

const latitude = parseFloat(parts[0]);
const longitude = parseFloat(parts[1]);

if (isNaN(latitude) || isNaN(longitude)) {
throw new Error('Invalid numeric values in Google Maps coordinate string');
}

return { latitude, longitude };
}

function validateCoordinates(latitude, longitude) {
if (typeof latitude !== 'number' || typeof longitude !== 'number') {
throw new Error('Coordinates must be numbers');
}

if (latitude < -90 || latitude > 90) {
throw new Error('Latitude must be between -90 and 90 degrees');
}

if (longitude < -180 || longitude > 180) {
throw new Error('Longitude must be between -180 and 180 degrees');
}

return true;
}

module.exports = {
parseDMS,
parseCoordinate,
parseCoordinatePair,
parseGoogleMapsCoords,
validateCoordinates,
getCoordinatesFromPayload: function(payload) {
const { latitude, longitude, coordinates } = payload || {};
let lat, lng;
if (typeof coordinates === 'string' && coordinates.length) {
const parsed = parseGoogleMapsCoords(coordinates);
lat = parsed.latitude;
lng = parsed.longitude;
} else if (latitude !== undefined && longitude !== undefined) {
const parsed = parseCoordinatePair(latitude, longitude);
lat = parsed.latitude;
lng = parsed.longitude;
} else {
throw new Error('Provide "coordinates" or both "latitude" and "longitude"');
}
validateCoordinates(lat, lng);
return { latitude: lat, longitude: lng };
}
};
104 changes: 90 additions & 14 deletions swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,46 @@ info:
description: Encode and decode DIGIPIN based on latitude and longitude
version: 1.0.0
servers:
- url: http://localhost:5000/api
- url: /api

paths:
/digipin/encode:
post:
summary: Encode lat/lon into DIGIPIN
summary: Encode coordinates into DIGIPIN
description: |
Encode coordinates into DIGIPIN. Supports decimal degrees, DMS (with ' or ′ for minutes and " or ″ or '' for seconds), and Google Maps coordinate string
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
latitude:
type: number
longitude:
type: number
oneOf:
- type: object
properties:
coordinates:
type: string
description: Google Maps coordinate string format
example: "12.903016468272682, 77.60606960542954"
required: [coordinates]
- type: object
properties:
latitude:
oneOf:
- type: number
description: Decimal degrees
example: 12.903016
- type: string
description: DMS format
example: "12°54''10.3\"N"
longitude:
oneOf:
- type: number
description: Decimal degrees
example: 77.606069
- type: string
description: DMS format
example: "77°36''22.3\"E"
required: [latitude, longitude]
responses:
'200':
description: Successfully encoded DIGIPIN
Expand All @@ -31,21 +54,62 @@ paths:
properties:
digipin:
type: string
description: The generated DIGIPIN code
example: "4P3-KCF-77M4"
input:
type: object
description: The parsed input coordinates in decimal degrees
properties:
latitude:
type: number
example: 12.903016468272682
longitude:
type: number
example: 77.60606960542954
'400':
description: Invalid input
get:
summary: Encode latitude and longitude into DIGIPIN
summary: Encode coordinates into DIGIPIN via query parameters
description: |
Encode coordinates into DIGIPIN using query parameters. Supports decimal degrees, DMS ('/′ minutes and "/″/'' seconds), and Google Maps coordinate string
parameters:
- in: query
name: coordinates
required: false
schema:
type: string
description: Google Maps coordinate string format (e.g., "12.903016468272682,77.60606960542954")
example: "12.903016468272682,77.60606960542954"
- in: query
name: latitude
required: true
required: false
schema:
type: number
oneOf:
- type: number
- type: string
description: Latitude in decimal degrees or DMS format
examples:
decimal:
value: 12.903016
summary: Decimal degrees
dms:
value: "12°54''10.3\"N"
summary: DMS format
- in: query
name: longitude
required: true
required: false
schema:
type: number
oneOf:
- type: number
- type: string
description: Longitude in decimal degrees or DMS format
examples:
decimal:
value: 77.606069
summary: Decimal degrees
dms:
value: "77°36''22.3\"E"
summary: DMS format
responses:
'200':
description: Successfully encoded DIGIPIN
Expand All @@ -56,8 +120,20 @@ paths:
properties:
digipin:
type: string
description: The generated DIGIPIN code
example: "4P3-KCF-77M4"
input:
type: object
description: The parsed input coordinates in decimal degrees
properties:
latitude:
type: number
example: 12.903016468272682
longitude:
type: number
example: 77.60606960542954
'400':
description: Invalid input
description: Invalid input or missing required parameters

/digipin/decode:
post:
Expand Down