From 8890e64d0ec366191ded5fa1539d898c48e83d46 Mon Sep 17 00:00:00 2001 From: "(jenkins)" <(jenkins)> Date: Fri, 17 Apr 2026 00:34:55 +0100 Subject: [PATCH] Optimise db query. Add more error detail. --- backend/app/server.js | 50 ++++++++++++++++++++---------------- frontend/src/App.jsx | 3 ++- frontend/src/services/api.js | 2 +- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/backend/app/server.js b/backend/app/server.js index d11d764..1f70af4 100644 --- a/backend/app/server.js +++ b/backend/app/server.js @@ -66,18 +66,27 @@ app.get('/api/line-of-sight', async (req, res) => { pathPoints.push(calculateDestination(startLat, startLon, bearing, dist)); } - // Batch water check: a point is "over water" if no city is within 500 km - const waterChecks = await Promise.all(pathPoints.map(async (p) => { - const checkQuery = ` - SELECT EXISTS ( - SELECT 1 FROM cities - WHERE ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, 500000) - LIMIT 1 - ) as has_land; - `; - const r = await pool.query(checkQuery, [p.lon, p.lat]); - return !r.rows[0].has_land; - })); + // Bulk land check: use a single query for all points to avoid connection pool exhaustion + const waterStart = Date.now(); + const waterCheckQuery = ` + WITH points AS ( + SELECT i, ST_SetSRID(ST_MakePoint(val[1], val[2]), 4326)::geography as p_geom + FROM unnest($1::float8[][]) WITH ORDINALITY AS t(val, i) + ) + SELECT i, EXISTS ( + SELECT 1 FROM cities + WHERE ST_DWithin(geom, p_geom, 500000) + LIMIT 1 + ) as has_land + FROM points + ORDER BY i; + `; + + const pointsArray = pathPoints.map(p => [p.lon, p.lat]); + const waterResults = await pool.query(waterCheckQuery, [pointsArray]); + const waterChecks = waterResults.rows.map(r => !r.has_land); + + console.log(`Bulk water check completed in ${Date.now() - waterStart}ms for ${pathPoints.length} points.`); const pathPointsWithWater = pathPoints.map((p, i) => ({ ...p, @@ -116,10 +125,13 @@ app.get('/api/line-of-sight', async (req, res) => { ORDER BY pos_on_line ASC; `; + const startTime = Date.now(); const result = await pool.query(query, [lineWKT, toleranceKm, startLon, startLat]); + const queryTime = Date.now() - startTime; + console.log(`Query completed in ${queryTime}ms. Found ${result.rows.length} candidates.`); - // Greedy dynamic deduplication: sort by population desc, accept a city only if - // it satisfies a distance threshold that depends on its importance (population). + const startDedupe = Date.now(); + // Greedy dynamic deduplication... const byPopulation = [...result.rows].sort((a, b) => (b.population || 0) - (a.population || 0)); const accepted = []; for (const city of byPopulation) { @@ -127,21 +139,14 @@ app.get('/api/line-of-sight', async (req, res) => { const tooClose = accepted.some(s => { const dist = haversineKm(s.lat, s.lon, city.lat, city.lon); const sPop = s.population || 0; - - // Major hubs (>1M) can be closer (30km) to each other (e.g., Tokyo/Yokohama) if (cityPop > 1000000 && sPop > 1000000) return dist < 30; - - // Large cities (>100k) need at least 50km space if (cityPop > 100000 || sPop > 100000) return dist < 50; - - // Smaller towns and villages need 80km space from any other accepted place - // to prevent clutter in high-density regions (like Europe/East Coast). - // Isolated towns (Alaska/Outback) will still show up as there's nothing else near them. return dist < 80; }); if (!tooClose) accepted.push(city); } accepted.sort((a, b) => a.pos_on_line - b.pos_on_line); + console.log(`Deduplication completed in ${Date.now() - startDedupe}ms. Final count: ${accepted.length}`); res.json({ success: true, @@ -159,6 +164,7 @@ app.get('/api/line-of-sight', async (req, res) => { line_coordinates: pathPointsWithWater } }); + console.log(`Request fully processed in ${Date.now() - startTime}ms`); } catch (err) { console.error('Database query error:', err); res.status(500).json({ success: false, error: 'Database query failed' }); diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 00ecc6e..b227a84 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -751,7 +751,8 @@ const APP = () => { renderLineOnMap(response.data.data); } catch (error) { console.error('Error fetching line of sight:', error); - setApiError('Failed to fetch route. Please try again.'); + const msg = error.response?.data?.error || error.message || 'Failed to fetch route.'; + setApiError(`Error: ${msg}`); } finally { setLoading(false); // Reset flight progress for a new line calculation diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 6800c49..534126b 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -4,7 +4,7 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || '/api'; const api = axios.create({ baseURL: API_BASE_URL, - timeout: 10000, + timeout: 30000, headers: { 'Content-Type': 'application/json' }