Got the automated address grabber working

This commit is contained in:
admin 2025-06-22 13:59:40 -06:00
parent e1ddebc90a
commit 5e2a6320ff
5 changed files with 420 additions and 65 deletions

View File

@ -24,6 +24,8 @@ COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./ COPY package*.json ./
COPY server.js ./ COPY server.js ./
COPY public ./public COPY public ./public
COPY routes ./routes
COPY services ./services
# Create non-root user # Create non-root user
RUN addgroup -g 1001 -S nodejs && \ RUN addgroup -g 1001 -S nodejs && \

View File

@ -579,8 +579,11 @@ function showAddLocationModal(lat, lng) {
addressInput.value = result.formattedAddress || result.fullAddress; addressInput.value = result.formattedAddress || result.fullAddress;
} else { } else {
addressInput.value = ''; // Clear if lookup fails 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 // Focus on first name input
@ -633,7 +636,12 @@ function editLocation(locationId) {
addressInput.value = result.formattedAddress || result.fullAddress; addressInput.value = result.formattedAddress || result.fullAddress;
} else if (!location['Address']) { } else if (!location['Address']) {
addressInput.value = ''; 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 // Reverse geocode to get address from coordinates
async function reverseGeocode(lat, lng) { async function reverseGeocode(lat, lng) {
try { try {
// Add a small delay to respect rate limits const response = await fetch(`/api/geocode/reverse?lat=${lat}&lng=${lng}`);
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'
}
});
if (!response.ok) { 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) { if (!result.success || !result.data) {
throw new Error(data.error); throw new Error('Geocoding failed');
} }
// Format the address from the response return result.data;
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
};
} catch (error) { } catch (error) {
console.error('Reverse geocoding error:', 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 // Manual address lookup for add form
function lookupAddressForAdd() { async function lookupAddressForAdd() {
const lat = parseFloat(document.getElementById('location-lat').value); const latInput = document.getElementById('location-lat');
const lng = parseFloat(document.getElementById('location-lng').value); const lngInput = document.getElementById('location-lng');
const addressInput = document.getElementById('location-address'); const addressInput = document.getElementById('location-address');
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng)) { if (!isNaN(lat) && !isNaN(lng)) {
addressInput.value = 'Looking up address...'; addressInput.value = 'Looking up address...';
reverseGeocode(lat, lng).then(result => { const result = await reverseGeocode(lat, lng);
if (result) { if (result) {
addressInput.value = result.formattedAddress || result.fullAddress; addressInput.value = result.formattedAddress || result.fullAddress;
showStatus('Address found!', 'success'); showStatus('Address found!', 'success');
} else { } else {
addressInput.value = ''; addressInput.value = '';
showStatus('Could not find address for these coordinates', 'error'); showStatus('Could not find address for these coordinates', 'warning');
} }
});
} else { } else {
showStatus('Please enter valid coordinates first', 'warning'); showStatus('Please enter valid coordinates first', 'warning');
} }
} }
// Manual address lookup for edit form // Manual address lookup for edit form
function lookupAddressForEdit() { async function lookupAddressForEdit() {
const lat = parseFloat(document.getElementById('edit-location-lat').value); const latInput = document.getElementById('edit-location-lat');
const lng = parseFloat(document.getElementById('edit-location-lng').value); const lngInput = document.getElementById('edit-location-lng');
const addressInput = document.getElementById('edit-location-address'); const addressInput = document.getElementById('edit-location-address');
const lat = parseFloat(latInput.value);
const lng = parseFloat(lngInput.value);
if (!isNaN(lat) && !isNaN(lng)) { if (!isNaN(lat) && !isNaN(lng)) {
addressInput.value = 'Looking up address...'; addressInput.value = 'Looking up address...';
reverseGeocode(lat, lng).then(result => { const result = await reverseGeocode(lat, lng);
if (result) { if (result) {
addressInput.value = result.formattedAddress || result.fullAddress; addressInput.value = result.formattedAddress || result.fullAddress;
showStatus('Address found!', 'success'); showStatus('Address found!', 'success');
} else { } else {
addressInput.value = ''; addressInput.value = '';
showStatus('Could not find address for these coordinates', 'error'); showStatus('Could not find address for these coordinates', 'warning');
} }
});
} else { } else {
showStatus('Please enter valid coordinates first', 'warning'); showStatus('Please enter valid coordinates first', 'warning');
} }

View 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;

View File

@ -7,6 +7,9 @@ const winston = require('winston');
const path = require('path'); const path = require('path');
require('dotenv').config(); require('dotenv').config();
// Import geocoding routes
const geocodingRoutes = require('./routes/geocoding');
// Parse project and table IDs from view URL // Parse project and table IDs from view URL
function parseNocoDBUrl(url) { function parseNocoDBUrl(url) {
if (!url) return { projectId: null, tableId: null }; 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(express.static(path.join(__dirname, 'public')));
app.use('/api/', limiter); app.use('/api/', limiter);
// Add geocoding routes
app.use('/api/geocode', geocodingRoutes);
// Health check endpoint // Health check endpoint
app.get('/health', (req, res) => { app.get('/health', (req, res) => {
res.json({ res.json({

View 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
};