sc/full-world-wrap #6
+28
-22
@@ -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 1 FROM cities
|
SELECT i, ST_SetSRID(ST_MakePoint(val[1], val[2]), 4326)::geography as p_geom
|
||||||
WHERE ST_DWithin(geom, ST_SetSRID(ST_MakePoint($1, $2), 4326)::geography, 500000)
|
FROM unnest($1::float8[][]) WITH ORDINALITY AS t(val, i)
|
||||||
LIMIT 1
|
)
|
||||||
) as has_land;
|
SELECT i, EXISTS (
|
||||||
`;
|
SELECT 1 FROM cities
|
||||||
const r = await pool.query(checkQuery, [p.lon, p.lat]);
|
WHERE ST_DWithin(geom, p_geom, 500000)
|
||||||
return !r.rows[0].has_land;
|
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) => ({
|
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' });
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user