Got the automated address grabber working
This commit is contained in:
parent
e1ddebc90a
commit
5e2a6320ff
@ -24,6 +24,8 @@ COPY --from=builder /app/node_modules ./node_modules
|
||||
COPY package*.json ./
|
||||
COPY server.js ./
|
||||
COPY public ./public
|
||||
COPY routes ./routes
|
||||
COPY services ./services
|
||||
|
||||
# Create non-root user
|
||||
RUN addgroup -g 1001 -S nodejs && \
|
||||
|
||||
@ -579,8 +579,11 @@ function showAddLocationModal(lat, lng) {
|
||||
addressInput.value = result.formattedAddress || result.fullAddress;
|
||||
} else {
|
||||
addressInput.value = ''; // Clear if lookup fails
|
||||
showStatus('Could not fetch address automatically', 'warning');
|
||||
// Don't show warning for automatic lookups
|
||||
}
|
||||
}).catch(error => {
|
||||
console.error('Address lookup failed:', error);
|
||||
addressInput.value = '';
|
||||
});
|
||||
|
||||
// Focus on first name input
|
||||
@ -633,7 +636,12 @@ function editLocation(locationId) {
|
||||
addressInput.value = result.formattedAddress || result.fullAddress;
|
||||
} else if (!location['Address']) {
|
||||
addressInput.value = '';
|
||||
// Don't show error - just silently fail
|
||||
}
|
||||
}).catch(error => {
|
||||
// Handle any unexpected errors
|
||||
console.error('Address lookup failed:', error);
|
||||
addressInput.value = '';
|
||||
});
|
||||
}
|
||||
|
||||
@ -923,53 +931,20 @@ window.addEventListener('beforeunload', () => {
|
||||
// Reverse geocode to get address from coordinates
|
||||
async function reverseGeocode(lat, lng) {
|
||||
try {
|
||||
// Add a small delay to respect rate limits
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
const response = await fetch(`https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`, {
|
||||
headers: {
|
||||
'User-Agent': 'NocoDB Map Viewer 1.0'
|
||||
}
|
||||
});
|
||||
const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Geocoding service unavailable');
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Geocoding service unavailable');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
const result = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error('Geocoding failed');
|
||||
}
|
||||
|
||||
// Format the address from the response
|
||||
const address = data.display_name || '';
|
||||
|
||||
// You can also extract specific components if needed
|
||||
const addressComponents = {
|
||||
house_number: data.address?.house_number || '',
|
||||
road: data.address?.road || '',
|
||||
suburb: data.address?.suburb || data.address?.neighbourhood || '',
|
||||
city: data.address?.city || data.address?.town || data.address?.village || '',
|
||||
state: data.address?.state || '',
|
||||
postcode: data.address?.postcode || '',
|
||||
country: data.address?.country || ''
|
||||
};
|
||||
|
||||
// Create a formatted address string
|
||||
let formattedAddress = '';
|
||||
if (addressComponents.house_number) formattedAddress += addressComponents.house_number + ' ';
|
||||
if (addressComponents.road) formattedAddress += addressComponents.road + ', ';
|
||||
if (addressComponents.suburb) formattedAddress += addressComponents.suburb + ', ';
|
||||
if (addressComponents.city) formattedAddress += addressComponents.city + ', ';
|
||||
if (addressComponents.state) formattedAddress += addressComponents.state + ' ';
|
||||
if (addressComponents.postcode) formattedAddress += addressComponents.postcode;
|
||||
|
||||
return {
|
||||
fullAddress: address,
|
||||
formattedAddress: formattedAddress.trim().replace(/,$/, ''),
|
||||
components: addressComponents
|
||||
};
|
||||
return result.data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Reverse geocoding error:', error);
|
||||
@ -977,45 +952,73 @@ async function reverseGeocode(lat, lng) {
|
||||
}
|
||||
}
|
||||
|
||||
// Add a new function for forward geocoding (address to coordinates)
|
||||
async function forwardGeocode(address) {
|
||||
try {
|
||||
const response = await fetch(`/api/geocode/forward?address=${encodeURIComponent(address)}`);
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.error || 'Geocoding service unavailable');
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (!result.success || !result.data) {
|
||||
throw new Error('Geocoding failed');
|
||||
}
|
||||
|
||||
return result.data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Forward geocoding error:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Manual address lookup for add form
|
||||
function lookupAddressForAdd() {
|
||||
const lat = parseFloat(document.getElementById('location-lat').value);
|
||||
const lng = parseFloat(document.getElementById('location-lng').value);
|
||||
async function lookupAddressForAdd() {
|
||||
const latInput = document.getElementById('location-lat');
|
||||
const lngInput = document.getElementById('location-lng');
|
||||
const addressInput = document.getElementById('location-address');
|
||||
|
||||
const lat = parseFloat(latInput.value);
|
||||
const lng = parseFloat(lngInput.value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
addressInput.value = 'Looking up address...';
|
||||
reverseGeocode(lat, lng).then(result => {
|
||||
const result = await reverseGeocode(lat, lng);
|
||||
if (result) {
|
||||
addressInput.value = result.formattedAddress || result.fullAddress;
|
||||
showStatus('Address found!', 'success');
|
||||
} else {
|
||||
addressInput.value = '';
|
||||
showStatus('Could not find address for these coordinates', 'error');
|
||||
showStatus('Could not find address for these coordinates', 'warning');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showStatus('Please enter valid coordinates first', 'warning');
|
||||
}
|
||||
}
|
||||
|
||||
// Manual address lookup for edit form
|
||||
function lookupAddressForEdit() {
|
||||
const lat = parseFloat(document.getElementById('edit-location-lat').value);
|
||||
const lng = parseFloat(document.getElementById('edit-location-lng').value);
|
||||
async function lookupAddressForEdit() {
|
||||
const latInput = document.getElementById('edit-location-lat');
|
||||
const lngInput = document.getElementById('edit-location-lng');
|
||||
const addressInput = document.getElementById('edit-location-address');
|
||||
|
||||
const lat = parseFloat(latInput.value);
|
||||
const lng = parseFloat(lngInput.value);
|
||||
|
||||
if (!isNaN(lat) && !isNaN(lng)) {
|
||||
addressInput.value = 'Looking up address...';
|
||||
reverseGeocode(lat, lng).then(result => {
|
||||
const result = await reverseGeocode(lat, lng);
|
||||
if (result) {
|
||||
addressInput.value = result.formattedAddress || result.fullAddress;
|
||||
showStatus('Address found!', 'success');
|
||||
} else {
|
||||
addressInput.value = '';
|
||||
showStatus('Could not find address for these coordinates', 'error');
|
||||
showStatus('Could not find address for these coordinates', 'warning');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
showStatus('Please enter valid coordinates first', 'warning');
|
||||
}
|
||||
|
||||
113
nocodb-map-viewer/app/routes/geocoding.js
Normal file
113
nocodb-map-viewer/app/routes/geocoding.js
Normal file
@ -0,0 +1,113 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const { reverseGeocode, forwardGeocode, 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
|
||||
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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
@ -7,6 +7,9 @@ const winston = require('winston');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
// Import geocoding routes
|
||||
const geocodingRoutes = require('./routes/geocoding');
|
||||
|
||||
// Parse project and table IDs from view URL
|
||||
function parseNocoDBUrl(url) {
|
||||
if (!url) return { projectId: null, tableId: null };
|
||||
@ -142,6 +145,9 @@ app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// Add geocoding routes
|
||||
app.use('/api/geocode', geocodingRoutes);
|
||||
|
||||
// Health check endpoint
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
|
||||
231
nocodb-map-viewer/app/services/geocoding.js
Normal file
231
nocodb-map-viewer/app/services/geocoding.js
Normal file
@ -0,0 +1,231 @@
|
||||
const axios = require('axios');
|
||||
const winston = require('winston');
|
||||
|
||||
// Configure logger
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp(),
|
||||
winston.format.json()
|
||||
),
|
||||
transports: [
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple()
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
// Cache for geocoding results (simple in-memory cache)
|
||||
const geocodeCache = new Map();
|
||||
const CACHE_TTL = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
// Clean up old cache entries periodically
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of geocodeCache.entries()) {
|
||||
if (now - value.timestamp > CACHE_TTL) {
|
||||
geocodeCache.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60 * 60 * 1000); // Run every hour
|
||||
|
||||
/**
|
||||
* Reverse geocode coordinates to get address
|
||||
* @param {number} lat - Latitude
|
||||
* @param {number} lng - Longitude
|
||||
* @returns {Promise<Object>} Geocoding result
|
||||
*/
|
||||
async function reverseGeocode(lat, lng) {
|
||||
// Create cache key
|
||||
const cacheKey = `${lat.toFixed(6)},${lng.toFixed(6)}`;
|
||||
|
||||
// Check cache first
|
||||
const cached = geocodeCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
logger.debug(`Geocoding cache hit for ${cacheKey}`);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
// Add delay to respect Nominatim rate limits (max 1 request per second)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
logger.info(`Reverse geocoding: ${lat}, ${lng}`);
|
||||
|
||||
const response = await axios.get('https://nominatim.openstreetmap.org/reverse', {
|
||||
params: {
|
||||
format: 'json',
|
||||
lat: lat,
|
||||
lon: lng,
|
||||
zoom: 18,
|
||||
addressdetails: 1,
|
||||
'accept-language': 'en'
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': 'NocoDB Map Viewer 1.0 (https://github.com/yourusername/nocodb-map-viewer)'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (response.data.error) {
|
||||
throw new Error(response.data.error);
|
||||
}
|
||||
|
||||
// Process the response
|
||||
const result = processGeocodeResponse(response.data);
|
||||
|
||||
// Cache the result
|
||||
geocodeCache.set(cacheKey, {
|
||||
data: result,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Reverse geocoding error:', error.message);
|
||||
|
||||
if (error.response?.status === 429) {
|
||||
throw new Error('Rate limit exceeded. Please try again later.');
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
throw new Error('Geocoding service timeout');
|
||||
} else {
|
||||
throw new Error('Geocoding service unavailable');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward geocode address to get coordinates
|
||||
* @param {string} address - Address to geocode
|
||||
* @returns {Promise<Object>} Geocoding result
|
||||
*/
|
||||
async function forwardGeocode(address) {
|
||||
// Create cache key
|
||||
const cacheKey = `addr:${address.toLowerCase()}`;
|
||||
|
||||
// Check cache first
|
||||
const cached = geocodeCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||
logger.debug(`Geocoding cache hit for ${cacheKey}`);
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
try {
|
||||
// Add delay to respect rate limits
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
logger.info(`Forward geocoding: ${address}`);
|
||||
|
||||
const response = await axios.get('https://nominatim.openstreetmap.org/search', {
|
||||
params: {
|
||||
format: 'json',
|
||||
q: address,
|
||||
limit: 1,
|
||||
addressdetails: 1,
|
||||
'accept-language': 'en'
|
||||
},
|
||||
headers: {
|
||||
'User-Agent': 'NocoDB Map Viewer 1.0 (https://github.com/yourusername/nocodb-map-viewer)'
|
||||
},
|
||||
timeout: 10000
|
||||
});
|
||||
|
||||
if (!response.data || response.data.length === 0) {
|
||||
throw new Error('No results found');
|
||||
}
|
||||
|
||||
// Process the first result
|
||||
const result = processGeocodeResponse(response.data[0]);
|
||||
|
||||
// Cache the result
|
||||
geocodeCache.set(cacheKey, {
|
||||
data: result,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
logger.error('Forward geocoding error:', error.message);
|
||||
|
||||
if (error.response?.status === 429) {
|
||||
throw new Error('Rate limit exceeded. Please try again later.');
|
||||
} else if (error.code === 'ECONNABORTED') {
|
||||
throw new Error('Geocoding service timeout');
|
||||
} else {
|
||||
throw new Error('Geocoding service unavailable');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process geocoding response into standardized format
|
||||
* @param {Object} data - Raw geocoding response
|
||||
* @returns {Object} Processed geocoding data
|
||||
*/
|
||||
function processGeocodeResponse(data) {
|
||||
// Extract address components
|
||||
const addressComponents = {
|
||||
house_number: data.address?.house_number || '',
|
||||
road: data.address?.road || '',
|
||||
suburb: data.address?.suburb || data.address?.neighbourhood || '',
|
||||
city: data.address?.city || data.address?.town || data.address?.village || '',
|
||||
state: data.address?.state || data.address?.province || '',
|
||||
postcode: data.address?.postcode || '',
|
||||
country: data.address?.country || ''
|
||||
};
|
||||
|
||||
// Create formatted address string
|
||||
let formattedAddress = '';
|
||||
if (addressComponents.house_number) formattedAddress += addressComponents.house_number + ' ';
|
||||
if (addressComponents.road) formattedAddress += addressComponents.road + ', ';
|
||||
if (addressComponents.suburb) formattedAddress += addressComponents.suburb + ', ';
|
||||
if (addressComponents.city) formattedAddress += addressComponents.city + ', ';
|
||||
if (addressComponents.state) formattedAddress += addressComponents.state + ' ';
|
||||
if (addressComponents.postcode) formattedAddress += addressComponents.postcode;
|
||||
|
||||
// Clean up formatting
|
||||
formattedAddress = formattedAddress.trim().replace(/,$/, '');
|
||||
|
||||
return {
|
||||
fullAddress: data.display_name || '',
|
||||
formattedAddress: formattedAddress,
|
||||
components: addressComponents,
|
||||
coordinates: {
|
||||
lat: parseFloat(data.lat),
|
||||
lng: parseFloat(data.lon)
|
||||
},
|
||||
boundingBox: data.boundingbox || null,
|
||||
placeId: data.place_id || null,
|
||||
osmType: data.osm_type || null,
|
||||
osmId: data.osm_id || null
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache statistics
|
||||
* @returns {Object} Cache statistics
|
||||
*/
|
||||
function getCacheStats() {
|
||||
return {
|
||||
size: geocodeCache.size,
|
||||
maxSize: 1000, // Could be made configurable
|
||||
ttl: CACHE_TTL
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the geocoding cache
|
||||
*/
|
||||
function clearCache() {
|
||||
geocodeCache.clear();
|
||||
logger.info('Geocoding cache cleared');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
reverseGeocode,
|
||||
forwardGeocode,
|
||||
getCacheStats,
|
||||
clearCache
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user