142 lines
4.4 KiB
JavaScript
142 lines
4.4 KiB
JavaScript
const express = require('express');
|
|
const cors = require('cors');
|
|
const { Pool } = require('pg');
|
|
require('dotenv').config();
|
|
|
|
const app = express();
|
|
const PORT = process.env.PORT || 3001;
|
|
|
|
// Database connection pool
|
|
const pool = new Pool({
|
|
connectionString: process.env.DATABASE_URL || 'postgresql://line_of_sight:line_of_sight_pass@postgres:5432/line_of_sight'
|
|
});
|
|
|
|
app.use(cors());
|
|
app.use(express.json());
|
|
|
|
// Helper to calculate destination point given start, bearing, and distance (km)
|
|
const calculateDestination = (lat, lon, bearing, distance) => {
|
|
const R = 6371;
|
|
const brng = (bearing * Math.PI) / 180;
|
|
const φ1 = (lat * Math.PI) / 180;
|
|
const λ1 = (lon * Math.PI) / 180;
|
|
const δ = distance / R;
|
|
|
|
const φ2 = Math.asin(
|
|
Math.sin(φ1) * Math.cos(δ) +
|
|
Math.cos(φ1) * Math.sin(δ) * Math.cos(brng)
|
|
);
|
|
const λ2 =
|
|
λ1 +
|
|
Math.atan2(
|
|
Math.sin(brng) * Math.sin(δ) * Math.cos(φ1),
|
|
Math.cos(δ) - Math.sin(φ1) * Math.sin(φ2)
|
|
);
|
|
|
|
return {
|
|
lat: (φ2 * 180) / Math.PI,
|
|
lon: (((λ2 * 180) / Math.PI + 540) % 360) - 180
|
|
};
|
|
};
|
|
|
|
// Real API endpoint - uses PostGIS for spatial queries
|
|
app.get('/api/line-of-sight', async (req, res) => {
|
|
const { lat, lon, direction, tolerance } = req.query;
|
|
|
|
const startLat = parseFloat(lat) || 51.5074;
|
|
const startLon = parseFloat(lon) || -0.1278;
|
|
const bearing = parseInt(direction) || 0;
|
|
const toleranceKm = parseInt(tolerance) || 50;
|
|
|
|
console.log(`Processing real request: lat=${startLat}, lon=${startLon}, bearing=${bearing}, tolerance=${toleranceKm}`);
|
|
|
|
try {
|
|
// Generate path points for visualization and spatial query
|
|
const pathPoints = [];
|
|
const totalDistance = 20000;
|
|
const steps = 80; // More steps for smoother speed transition
|
|
|
|
for (let i = 0; i <= steps; i++) {
|
|
const dist = (totalDistance * i) / steps;
|
|
pathPoints.push(calculateDestination(startLat, startLon, bearing, dist));
|
|
}
|
|
|
|
// Batch check for 'over water' status for all path points
|
|
// We'll consider a point 'over water' if no city is within 500km
|
|
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 res = await pool.query(checkQuery, [p.lon, p.lat]);
|
|
return !res.rows[0].has_land;
|
|
}));
|
|
|
|
const pathPointsWithWater = pathPoints.map((p, i) => ({
|
|
...p,
|
|
is_over_water: waterChecks[i]
|
|
}));
|
|
|
|
const lineWKT = `LINESTRING(${pathPoints.map(p => `${p.lon} ${p.lat}`).join(',')})`;
|
|
|
|
const query = `
|
|
WITH path AS (
|
|
SELECT ST_GeogFromText($1) as route,
|
|
ST_MakePoint($3, $4)::geography as start_node
|
|
)
|
|
SELECT
|
|
id,
|
|
name,
|
|
population,
|
|
country,
|
|
ST_Y(geom::geometry) as lat,
|
|
ST_X(geom::geometry) as lon,
|
|
ST_Distance(geom, (SELECT route FROM path)) / 1000 as distance_off_line_km,
|
|
ST_Distance(geom, (SELECT start_node FROM path)) / 1000 as distance_from_start_km,
|
|
ST_LineLocatePoint((SELECT route FROM path)::geometry, geom::geometry) as pos_on_line
|
|
FROM cities
|
|
WHERE ST_DWithin(geom, (SELECT route FROM path), $2 * 1000)
|
|
ORDER BY pos_on_line ASC
|
|
LIMIT 200;
|
|
`;
|
|
|
|
const result = await pool.query(query, [lineWKT, toleranceKm, startLon, startLat]);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
start_point: { lat: startLat, lon: startLon },
|
|
direction: bearing,
|
|
tolerance_km: toleranceKm,
|
|
conurbations: result.rows.map(row => ({
|
|
...row,
|
|
name: row.name || 'Unknown',
|
|
country: row.country || 'Unknown',
|
|
distance_km: Math.round(row.distance_from_start_km),
|
|
off_line_km: Math.round(row.distance_off_line_km)
|
|
})),
|
|
line_coordinates: pathPointsWithWater
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error('Database query error:', err);
|
|
res.status(500).json({ success: false, error: 'Database query failed' });
|
|
}
|
|
});
|
|
|
|
// Health check endpoint
|
|
app.get('/api/health', (req, res) => {
|
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
|
});
|
|
|
|
if (require.main === module) {
|
|
app.listen(PORT, '0.0.0.0', () => {
|
|
console.log(`Line of Sight Backend running on port ${PORT}`);
|
|
});
|
|
}
|
|
|
|
module.exports = { app, calculateDestination };
|