223 lines
6.1 KiB
JavaScript
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;
|