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
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
PORT=5000
NODE_ENV=development
19 changes: 11 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,27 @@
{
"name": "digipin",
"version": "1.0.0",
"description": "",
"description": "DIGIPIN API - Geospatial Addressing Solution by India Post",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"keywords": ["digipin", "geocoding", "india-post", "geospatial"],
"author": "Department of Posts, Government of India",
"license": "Apache-2.0",
"dependencies": {
"cors": "^2.8.5",
"digipin": "file:",
"dotenv": "^16.4.7",
"express": "^5.1.0",
"morgan": "^1.10.0",
"swagger-ui-express": "^5.0.1",
"yaml": "^2.7.1",
"yamljs": "^0.3.0"
"yaml": "^2.7.1"
},
"devDependencies": {
"nodemon": "^3.0.0"
},
"engines": {
"node": ">=14.0.0"
}
}
16 changes: 14 additions & 2 deletions server.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ const app = require('./src/app');

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
console.log(`DIGIPIN API is running and API docs can be found at at http://localhost:${PORT}/api-docs`);
const server = app.listen(PORT, () => {
console.log(`DIGIPIN API is running and API docs can be found at http://localhost:${PORT}/api-docs`);
});

process.on('SIGTERM', () => {
server.close(() => {
console.log('Server closed');
});
});

process.on('SIGINT', () => {
server.close(() => {
console.log('Server closed');
});
});
26 changes: 23 additions & 3 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,40 @@ const YAML = require('yaml');
const fs = require('fs');
const path = require('path');

// const swaggerDocument = YAML.load(path.join(__dirname, '../swagger.yaml'));
const swaggerDocument = YAML.parse(fs.readFileSync(path.join(__dirname, '../swagger.yaml'), 'utf8'));

const app = express();

// Middleware
app.use(cors());
app.use(express.json());
app.use(morgan('dev'));
app.use(express.json({ limit: '10kb' }));
app.use(morgan(process.env.NODE_ENV === 'production' ? 'combined' : 'dev'));

// Security headers
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
next();
});

// Swagger Docs Route
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

// DIGIPIN API Routes
app.use('/api/digipin', digipinRoutes);

// 404 handler
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});

// Error handler
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production' ? 'Internal server error' : err.message
});
});

module.exports = app;
115 changes: 59 additions & 56 deletions src/digipin.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,51 @@
*/

const DIGIPIN_GRID = [
['F', 'C', '9', '8'],
['J', '3', '2', '7'],
['K', '4', '5', '6'],
['L', 'M', 'P', 'T']
];

const BOUNDS = {
minLat: 2.5,
maxLat: 38.5,
minLon: 63.5,
maxLon: 99.5
};
['F', 'C', '9', '8'],
['J', '3', '2', '7'],
['K', '4', '5', '6'],
['L', 'M', 'P', 'T']
];

const BOUNDS = {
minLat: 2.5,
maxLat: 38.5,
minLon: 63.5,
maxLon: 99.5
};

// Create lookup map for O(1) character position lookup
const CHAR_LOOKUP = {};
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
CHAR_LOOKUP[DIGIPIN_GRID[r][c]] = { row: r, col: c };
}
}

function getDigiPin(lat, lon) {
if (lat < BOUNDS.minLat || lat > BOUNDS.maxLat) throw new Error('Latitude out of range');
if (lon < BOUNDS.minLon || lon > BOUNDS.maxLon) throw new Error('Longitude out of range');
if (typeof lat !== 'number' || typeof lon !== 'number' || isNaN(lat) || isNaN(lon)) {
throw new Error('Invalid coordinates: must be numbers');
}
if (lat < BOUNDS.minLat || lat > BOUNDS.maxLat) {
throw new Error(`Latitude out of range (${BOUNDS.minLat} to ${BOUNDS.maxLat})`);
}
if (lon < BOUNDS.minLon || lon > BOUNDS.maxLon) {
throw new Error(`Longitude out of range (${BOUNDS.minLon} to ${BOUNDS.maxLon})`);
}

let minLat = BOUNDS.minLat;
let maxLat = BOUNDS.maxLat;
let minLon = BOUNDS.minLon;
let maxLon = BOUNDS.maxLon;

let digiPin = '';
const GRID_SIZE = 4;
const MAX_LEVEL = 10;

for (let level = 1; level <= 10; level++) {
const latDiv = (maxLat - minLat) / 4;
const lonDiv = (maxLon - minLon) / 4;
for (let level = 1; level <= MAX_LEVEL; level++) {
const latDiv = (maxLat - minLat) / GRID_SIZE;
const lonDiv = (maxLon - minLon) / GRID_SIZE;

// REVERSED row logic (to match original)
let row = 3 - Math.floor((lat - minLat) / latDiv);
let col = Math.floor((lon - minLon) / lonDiv);

Expand All @@ -48,8 +64,7 @@ const DIGIPIN_GRID = [

if (level === 3 || level === 6) digiPin += '-';

// Update bounds (reverse logic for row)
maxLat = minLat + latDiv * (4 - row);
maxLat = minLat + latDiv * (GRID_SIZE - row);
minLat = minLat + latDiv * (3 - row);

minLon = minLon + lonDiv * col;
Expand All @@ -61,55 +76,43 @@ const DIGIPIN_GRID = [


function getLatLngFromDigiPin(digiPin) {
const pin = digiPin.replace(/-/g, '');
if (pin.length !== 10) throw new Error('Invalid DIGIPIN');
if (typeof digiPin !== 'string') {
throw new Error('Invalid DIGIPIN: must be a string');
}

const pin = digiPin.replace(/-/g, '').toUpperCase();
const EXPECTED_LENGTH = 10;

if (pin.length !== EXPECTED_LENGTH) {
throw new Error(`Invalid DIGIPIN: must be ${EXPECTED_LENGTH} characters`);
}

let minLat = BOUNDS.minLat;
let maxLat = BOUNDS.maxLat;
let minLon = BOUNDS.minLon;
let maxLon = BOUNDS.maxLon;
const GRID_SIZE = 4;

for (let i = 0; i < 10; i++) {
for (let i = 0; i < EXPECTED_LENGTH; i++) {
const char = pin[i];
let found = false;
let ri = -1, ci = -1;

// Locate character in DIGIPIN grid
for (let r = 0; r < 4; r++) {
for (let c = 0; c < 4; c++) {
if (DIGIPIN_GRID[r][c] === char) {
ri = r;
ci = c;
found = true;
break;
}
}
if (found) break;
}

if (!found) throw new Error('Invalid character in DIGIPIN');
const pos = CHAR_LOOKUP[char];

const latDiv = (maxLat - minLat) / 4;
const lonDiv = (maxLon - minLon) / 4;
if (!pos) {
throw new Error(`Invalid character '${char}' in DIGIPIN`);
}

const lat1 = maxLat - latDiv * (ri + 1);
const lat2 = maxLat - latDiv * ri;
const lon1 = minLon + lonDiv * ci;
const lon2 = minLon + lonDiv * (ci + 1);
const latDiv = (maxLat - minLat) / GRID_SIZE;
const lonDiv = (maxLon - minLon) / GRID_SIZE;

// Update bounding box for next level
minLat = lat1;
maxLat = lat2;
minLon = lon1;
maxLon = lon2;
minLat = maxLat - latDiv * (pos.row + 1);
maxLat = maxLat - latDiv * pos.row;
minLon = minLon + lonDiv * pos.col;
maxLon = minLon + lonDiv;
}

const centerLat = (minLat + maxLat) / 2;
const centerLon = (minLon + maxLon) / 2;

return {
latitude: centerLat.toFixed(6),
longitude: centerLon.toFixed(6)
latitude: ((minLat + maxLat) / 2).toFixed(6),
longitude: ((minLon + maxLon) / 2).toFixed(6)
};
}

Expand Down
77 changes: 56 additions & 21 deletions src/routes/digipin.routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,64 @@ const express = require('express');
const router = express.Router();
const { getDigiPin, getLatLngFromDigiPin } = require('../digipin');

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, next) => {
const { latitude, longitude } = req.body;

if (!latitude || !longitude) {
return res.status(400).json({ error: 'Latitude and longitude are required' });
}

try {
const code = getDigiPin(parseFloat(latitude), parseFloat(longitude));
res.json({ digipin: code });
} catch (e) {
next(e);
}
});

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.post('/decode', (req, res, next) => {
const { digipin } = req.body;

if (!digipin) {
return res.status(400).json({ error: 'DIGIPIN is required' });
}

try {
const coords = getLatLngFromDigiPin(digipin);
res.json(coords);
} catch (e) {
next(e);
}
});

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('/decode', (req, res) => {
const { digipin } = req.query;
try {
const coords = getLatLngFromDigiPin(digipin);
res.json(coords);
} catch (e) {
res.status(400).json({ error: e.message });
}
});
router.get('/encode', (req, res, next) => {
const { latitude, longitude } = req.query;

if (!latitude || !longitude) {
return res.status(400).json({ error: 'Latitude and longitude are required' });
}

try {
const code = getDigiPin(parseFloat(latitude), parseFloat(longitude));
res.json({ digipin: code });
} catch (e) {
next(e);
}
});

router.get('/decode', (req, res, next) => {
const { digipin } = req.query;

if (!digipin) {
return res.status(400).json({ error: 'DIGIPIN is required' });
}

try {
const coords = getLatLngFromDigiPin(digipin);
res.json(coords);
} catch (e) {
next(e);
}
});

module.exports = router;