sc/full-world-wrap #6

Open
steve-admin wants to merge 17 commits from sc/full-world-wrap into main
3 changed files with 31 additions and 24 deletions
Showing only changes of commit 8890e64d0e - Show all commits
+25 -19
View File
@@ -66,18 +66,27 @@ app.get('/api/line-of-sight', async (req, res) => {
pathPoints.push(calculateDestination(startLat, startLon, bearing, dist)); pathPoints.push(calculateDestination(startLat, startLon, bearing, dist));
} }
// Batch water check: a point is "over water" if no city is within 500 km // Bulk land check: use a single query for all points to avoid connection pool exhaustion
const waterChecks = await Promise.all(pathPoints.map(async (p) => { const waterStart = Date.now();
const checkQuery = ` const waterCheckQuery = `
SELECT EXISTS ( 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 SELECT 1 FROM cities
WHERE ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, 500000) WHERE ST_DWithin(geom, p_geom, 500000)
LIMIT 1 LIMIT 1
) as has_land; ) as has_land
FROM points
ORDER BY i;
`; `;
const r = await pool.query(checkQuery, [p.lon, p.lat]);
return !r.rows[0].has_land; 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) => ({ const pathPointsWithWater = pathPoints.map((p, i) => ({
...p, ...p,
@@ -116,10 +125,13 @@ app.get('/api/line-of-sight', async (req, res) => {
ORDER BY pos_on_line ASC; ORDER BY pos_on_line ASC;
`; `;
const startTime = Date.now();
const result = await pool.query(query, [lineWKT, toleranceKm, startLon, startLat]); 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 const startDedupe = Date.now();
// it satisfies a distance threshold that depends on its importance (population). // Greedy dynamic deduplication...
const byPopulation = [...result.rows].sort((a, b) => (b.population || 0) - (a.population || 0)); const byPopulation = [...result.rows].sort((a, b) => (b.population || 0) - (a.population || 0));
const accepted = []; const accepted = [];
for (const city of byPopulation) { for (const city of byPopulation) {
@@ -127,21 +139,14 @@ app.get('/api/line-of-sight', async (req, res) => {
const tooClose = accepted.some(s => { const tooClose = accepted.some(s => {
const dist = haversineKm(s.lat, s.lon, city.lat, city.lon); const dist = haversineKm(s.lat, s.lon, city.lat, city.lon);
const sPop = s.population || 0; 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; if (cityPop > 1000000 && sPop > 1000000) return dist < 30;
// Large cities (>100k) need at least 50km space
if (cityPop > 100000 || sPop > 100000) return dist < 50; 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; return dist < 80;
}); });
if (!tooClose) accepted.push(city); if (!tooClose) accepted.push(city);
} }
accepted.sort((a, b) => a.pos_on_line - b.pos_on_line); 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({ res.json({
success: true, success: true,
@@ -159,6 +164,7 @@ app.get('/api/line-of-sight', async (req, res) => {
line_coordinates: pathPointsWithWater line_coordinates: pathPointsWithWater
} }
}); });
console.log(`Request fully processed in ${Date.now() - startTime}ms`);
} catch (err) { } catch (err) {
console.error('Database query error:', err); console.error('Database query error:', err);
res.status(500).json({ success: false, error: 'Database query failed' }); res.status(500).json({ success: false, error: 'Database query failed' });
+2 -1
View File
@@ -751,7 +751,8 @@ const APP = () => {
renderLineOnMap(response.data.data); renderLineOnMap(response.data.data);
} catch (error) { } catch (error) {
console.error('Error fetching line of sight:', 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 { } finally {
setLoading(false); setLoading(false);
// Reset flight progress for a new line calculation // Reset flight progress for a new line calculation
+1 -1
View File
@@ -4,7 +4,7 @@ const API_BASE_URL = import.meta.env.VITE_API_URL || '/api';
const api = axios.create({ const api = axios.create({
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
timeout: 10000, timeout: 30000,
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }