freealberta/map/app/routes/geocoding.js

223 lines
6.1 KiB
JavaScript

const express = require('express');
const router = express.Router();
const rateLimit = require('express-rate-limit');
const { reverseGeocode, forwardGeocode, forwardGeocodeSearch, getCacheStats } = require('../services/geocoding');
// Rate limiter specifically for geocoding endpoints
const geocodeLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 30, // limit each IP to 30 requests per windowMs
trustProxy: true, // Explicitly trust proxy
keyGenerator: (req) => {
return req.headers['cf-connecting-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0] ||
req.ip;
},
message: 'Too many geocoding requests, please try again later.'
});
/**
* Reverse geocode endpoint
* GET /api/geocode/reverse?lat=<latitude>&lng=<longitude>
*/
router.get('/reverse', geocodeLimiter, async (req, res) => {
try {
const { lat, lng } = req.query;
// Validate input
if (!lat || !lng) {
return res.status(400).json({
success: false,
error: 'Latitude and longitude are required'
});
}
const latitude = parseFloat(lat);
const longitude = parseFloat(lng);
if (isNaN(latitude) || isNaN(longitude)) {
return res.status(400).json({
success: false,
error: 'Invalid latitude or longitude'
});
}
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
return res.status(400).json({
success: false,
error: 'Coordinates out of range'
});
}
// Perform reverse geocoding
const result = await reverseGeocode(latitude, longitude);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Reverse geocoding error:', error);
const statusCode = error.message.includes('Rate limit') ? 429 : 500;
res.status(statusCode).json({
success: false,
error: error.message
});
}
});
/**
* Forward geocode endpoint
* GET /api/geocode/forward?address=<address>
*/
router.get('/forward', geocodeLimiter, async (req, res) => {
try {
const { address } = req.query;
// Validate input
if (!address || address.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Address is required'
});
}
// Perform forward geocoding
const result = await forwardGeocode(address);
res.json({
success: true,
data: result
});
} catch (error) {
console.error('Forward geocoding error:', error);
const statusCode = error.message.includes('Rate limit') ? 429 : 500;
res.status(statusCode).json({
success: false,
error: error.message
});
}
});
/**
* Forward geocode search endpoint (returns multiple results)
* GET /api/geocode/search?query=<address>&limit=<number>
*/
router.get('/search', geocodeLimiter, async (req, res) => {
try {
const { query, limit } = req.query;
// Validate input
if (!query || query.trim().length === 0) {
return res.status(400).json({
success: false,
error: 'Search query is required'
});
}
// Minimum search length
if (query.trim().length < 2) {
return res.json({
success: true,
data: []
});
}
const searchLimit = parseInt(limit) || 5;
if (searchLimit < 1 || searchLimit > 10) {
return res.status(400).json({
success: false,
error: 'Limit must be between 1 and 10'
});
}
// Perform forward geocoding search
const results = await forwardGeocodeSearch(query, searchLimit);
res.json({
success: true,
data: results,
query: query,
count: results.length
});
} catch (error) {
console.error('Forward geocoding search error:', error);
const statusCode = error.message.includes('Rate limit') ? 429 : 500;
res.status(statusCode).json({
success: false,
error: error.message
});
}
});
/**
* Get geocoding provider status
* GET /api/geocode/provider-status
*/
router.get('/provider-status', (req, res) => {
try {
const providers = [
{
name: 'Mapbox',
available: !!(process.env.MAPBOX_ACCESS_TOKEN || process.env.MAPBOX_API_KEY),
type: 'premium'
},
{
name: 'Nominatim',
available: true,
type: 'free'
},
{
name: 'Photon',
available: true,
type: 'free'
},
{
name: 'LocationIQ',
available: !!process.env.LOCATIONIQ_API_KEY,
type: 'premium'
},
{
name: 'ArcGIS',
available: true,
type: 'free'
}
];
res.json({
success: true,
providers: providers,
hasPremium: providers.some(p => p.available && p.type === 'premium')
});
} catch (error) {
console.error('Error checking provider status:', error);
res.status(500).json({
success: false,
error: 'Failed to check provider status'
});
}
});
/**
* Get geocoding cache statistics (admin endpoint)
* GET /api/geocode/cache/stats
*/
router.get('/cache/stats', (req, res) => {
res.json({
success: true,
data: getCacheStats()
});
});
module.exports = router;