Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c26f458df7 | |||
| 8d646ecd74 | |||
| 8890e64d0e | |||
| 411d10bbc6 | |||
| 76eed69c8f | |||
| 0bba5b1abe | |||
| 6a2f8856fe | |||
| 394394b387 | |||
| eda4697b03 | |||
| 29048e9d2a | |||
| 0826c606ba | |||
| e5413b4f0c | |||
| b1dc09c714 | |||
| 62959398d5 | |||
| b98bac9cff | |||
| c83049aca5 | |||
| 234ed8ae94 | |||
| bd5f5b98cc | |||
| 205e2d5be5 | |||
| 86aefba065 | |||
| 228032c913 | |||
| cac76a2f69 | |||
| bd9e92f304 |
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"permissions": {
|
||||||
|
"allow": [
|
||||||
|
"Bash(npm test:*)",
|
||||||
|
"Bash(npm run:*)",
|
||||||
|
"Bash(npx playwright:*)",
|
||||||
|
"Bash(node -e ':*)"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
# cache: ''npm''
|
||||||
|
# cache-dependency-path: backend/package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd backend && npm ci
|
||||||
|
- name: Run tests
|
||||||
|
run: cd backend && npm test
|
||||||
|
|
||||||
|
frontend-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
# cache: 'npm'
|
||||||
|
# cache-dependency-path: frontend/package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && npm ci
|
||||||
|
- name: Run unit tests
|
||||||
|
run: cd frontend && npm test -- --run
|
||||||
|
|
||||||
|
e2e-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.58.2-noble
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: '24'
|
||||||
|
# cache: 'npm'
|
||||||
|
# cache-dependency-path: frontend/package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
run: cd frontend && npm install
|
||||||
|
- name: Run E2E tests
|
||||||
|
run: cd frontend && npm run test:e2e
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Copilot Instructions for line-of-sight
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
This repository is a full-stack geospatial app:
|
||||||
|
- `frontend/`: React + Vite + MapLibre UI
|
||||||
|
- `backend/`: Express API with PostGIS queries
|
||||||
|
- `docker/` and `docker-compose.yml`: local orchestration and DB initialization
|
||||||
|
|
||||||
|
Prefer small, focused changes that keep frontend/backend contracts stable.
|
||||||
|
|
||||||
|
## First Commands to Know
|
||||||
|
Use these commands first when validating changes.
|
||||||
|
|
||||||
|
### Docker-first workflow (preferred)
|
||||||
|
- Start stack: `docker-compose up --build`
|
||||||
|
- Stop stack: `docker-compose down`
|
||||||
|
- Backend tests: `docker-compose exec backend npm test`
|
||||||
|
- Frontend unit tests: `docker-compose exec frontend npm test -- --run`
|
||||||
|
- Frontend E2E tests: `docker-compose exec frontend npm run test:e2e`
|
||||||
|
- Seed geodata: `docker-compose exec backend npm run seed-data`
|
||||||
|
|
||||||
|
### Local workflow (without Docker)
|
||||||
|
- Root Node version: `nvm use` (uses `.nvmrc`, currently Node 24)
|
||||||
|
- Backend dev: `cd backend && npm install && npm run dev`
|
||||||
|
- Frontend dev: `cd frontend && npm install && npm run start`
|
||||||
|
|
||||||
|
## Architecture and Boundaries
|
||||||
|
- Backend API entrypoint: `backend/app/server.js`
|
||||||
|
- Defines `GET /api/line-of-sight` and `GET /api/health`
|
||||||
|
- Performs destination/path generation and PostGIS querying
|
||||||
|
- Frontend app entrypoint: `frontend/src/App.jsx`
|
||||||
|
- Owns map lifecycle, animation, and UI state
|
||||||
|
- Calls backend via `frontend/src/services/api.js`
|
||||||
|
- Database bootstrap: `docker/init.sql`
|
||||||
|
- Enables PostGIS and creates `cities` with spatial index
|
||||||
|
|
||||||
|
When changing behavior, prefer backend geospatial logic over duplicating calculations in the frontend.
|
||||||
|
|
||||||
|
## Project Conventions
|
||||||
|
- Keep geospatial coordinates in WGS84 (`SRID 4326`) and preserve existing lat/lon parameter naming.
|
||||||
|
- Keep API responses JSON-compatible and backward compatible unless explicitly requested.
|
||||||
|
- Keep frontend styles in `frontend/src/styles/` (project uses custom CSS, not utility CSS frameworks).
|
||||||
|
- Avoid broad refactors in `App.jsx` unless task requires it; map lifecycle and refs are tightly coupled.
|
||||||
|
|
||||||
|
## Testing Expectations
|
||||||
|
- Backend tests live in `backend/tests/` and use Jest + Supertest.
|
||||||
|
- Frontend unit tests live in `frontend/src/__tests__/` and use Vitest + Testing Library.
|
||||||
|
- E2E tests live in `frontend/e2e/` and use Playwright.
|
||||||
|
- For feature changes:
|
||||||
|
- Run the closest unit tests first.
|
||||||
|
- Run E2E tests when user flows or map interactions are changed.
|
||||||
|
|
||||||
|
## Common Pitfalls
|
||||||
|
- PostGIS must be available before backend geospatial endpoints are exercised.
|
||||||
|
- `docker-compose.yml` pins Postgres service to `linux/arm64`; this can affect non-ARM hosts.
|
||||||
|
- Data import script (`backend/scripts/import_cities.js`) depends on `wget` and `unzip` availability.
|
||||||
|
- `README.md` contains roadmap/history notes; trust current scripts/config in source files first.
|
||||||
|
|
||||||
|
## CI Notes
|
||||||
|
CI config is in `.gitea/workflows/test.yml`.
|
||||||
|
- Backend and frontend tests run on Node 24.
|
||||||
|
- Frontend unit tests run with `npm test -- --run`.
|
||||||
|
- E2E runs in Playwright container.
|
||||||
|
|
||||||
|
## Link, Do Not Duplicate
|
||||||
|
For detailed setup and product context, reference:
|
||||||
|
- `README.md`
|
||||||
|
- `GEMINI.md`
|
||||||
|
|
||||||
|
Keep this file focused on actionable agent guidance and repo-specific guardrails.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## What This Project Is
|
||||||
|
|
||||||
|
**Line of Sight** is a geospatial web app that draws a great-circle line from a user-selected point on Earth in a chosen direction, then finds all cities along that path. It includes a flight-over animation that speeds up 20x over water and slows to normal speed over land.
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
### Docker (preferred workflow)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up --build # Start all services (frontend :3050, backend :3051, postgres)
|
||||||
|
docker-compose down # Stop all services
|
||||||
|
docker-compose logs -f # View logs
|
||||||
|
|
||||||
|
# Run tests inside containers
|
||||||
|
docker-compose exec backend npm test
|
||||||
|
docker-compose exec frontend npm test -- --run
|
||||||
|
docker-compose exec frontend npm run test:e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local development (without Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend
|
||||||
|
cd backend && npm run dev # Nodemon dev server on :3051
|
||||||
|
|
||||||
|
# Frontend
|
||||||
|
cd frontend && npm run start # Vite dev server on :3050
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backend unit tests (Jest + Supertest)
|
||||||
|
cd backend && npm test
|
||||||
|
|
||||||
|
# Frontend unit tests (Vitest)
|
||||||
|
cd frontend && npm test # Watch mode
|
||||||
|
cd frontend && npm test -- --run # Single run (CI mode)
|
||||||
|
|
||||||
|
# E2E tests (Playwright)
|
||||||
|
cd frontend && npm run test:e2e
|
||||||
|
|
||||||
|
# Seed the database with GeoNames city data
|
||||||
|
cd backend && npm run seed-data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend && npm run build # Vite production build → frontend/build/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a monorepo with three services orchestrated by Docker Compose:
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/ → React 19 + Vite 6 SPA (MapLibre GL, Turf.js, Axios)
|
||||||
|
backend/ → Node 24 + Express 5 REST API
|
||||||
|
docker/ → PostgreSQL + PostGIS initialization
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data flow
|
||||||
|
|
||||||
|
1. User clicks map → sets start point (lat/lon)
|
||||||
|
2. User selects direction (0–360°) and tolerance (km radius)
|
||||||
|
3. Frontend calls `GET /api/line-of-sight?lat=&lon=&direction=&tolerance=`
|
||||||
|
4. Backend generates 80 path points along a great-circle arc (up to 20,000 km)
|
||||||
|
5. For each point, a PostGIS `ST_DWithin()` query finds cities within tolerance
|
||||||
|
6. Backend also checks whether each point is over land (500 km radius land check)
|
||||||
|
7. Response includes line coordinates with per-point water flags and matching cities
|
||||||
|
8. Frontend renders the path on a 3D globe map and shows city markers
|
||||||
|
|
||||||
|
### Backend: `backend/app/server.js`
|
||||||
|
|
||||||
|
Single-file Express app. Key internals:
|
||||||
|
- `calculateDestination()` — Haversine formula for great-circle destination points
|
||||||
|
- `GET /api/line-of-sight` — main endpoint: path generation + PostGIS city lookup
|
||||||
|
- `GET /api/health` — health check
|
||||||
|
- Returns up to 200 cities ordered by position along the line
|
||||||
|
|
||||||
|
### Frontend: `frontend/src/App.jsx`
|
||||||
|
|
||||||
|
Single large React component (~687 lines) managing all state and map logic:
|
||||||
|
- MapLibre GL map with 3D globe projection and terrain/sky effects
|
||||||
|
- Direction slider drives a live preview line before the user commits
|
||||||
|
- After "Show Line of Sight", map locks (prevents moving start point)
|
||||||
|
- Flight animation uses Turf.js to interpolate positions along the path; speed is 1× over land, 20× over water with smooth acceleration (0.005 step), predictive look-ahead of 2000 km
|
||||||
|
- New cities are fetched every 2000 km during flight via the same API
|
||||||
|
- Three map styles: light, dark, satellite (Esri tiles)
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
PostGIS `cities` table with a GIST index on `geom GEOGRAPHY(POINT, 4326)`. All coordinates use WGS84 / SRID 4326. The init SQL seeds 10 major cities; the `seed-data` script imports from GeoNames for a fuller dataset.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **Geospatial**: WGS84 / SRID 4326 everywhere; PostGIS `GEOGRAPHY` type for distance queries
|
||||||
|
- **API**: RESTful; query params for `GET /api/line-of-sight`; env var `VITE_API_URL` (frontend) or `DATABASE_URL` (backend)
|
||||||
|
- **Styling**: Plain CSS files in `frontend/src/styles/` — no CSS framework
|
||||||
|
- **Tests**: Backend mocks the `pg` Pool; frontend mocks MapLibre GL and the API service. E2E tests use Playwright with a mock API response
|
||||||
|
|
||||||
|
## CI
|
||||||
|
|
||||||
|
Gitea workflow (`.gitea/workflows/test.yml`) runs on push/PR to `main`:
|
||||||
|
1. **backend-test** — `npm ci && npm test`
|
||||||
|
2. **frontend-test** — `npm ci && npm test -- --run`
|
||||||
|
3. **e2e-test** — uses `mcr.microsoft.com/playwright:v1.50.1` container
|
||||||
@@ -68,6 +68,17 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 📊 Data Management
|
||||||
|
The project includes a compressed dataset of ~68,000 cities from Natural Earth.
|
||||||
|
|
||||||
|
| Action | Command |
|
||||||
|
| :--- | :--- |
|
||||||
|
| **Import/Refresh Data** | `./import-cities.sh` |
|
||||||
|
|
||||||
|
*Note: The script will truncate the `cities` table and perform a fresh import of the global dataset stored in `docker/data/cities.csv.gz`.*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 📝 Roadmap Highlights
|
## 📝 Roadmap Highlights
|
||||||
- [ ] Implement real ST_DWithin() PostGIS queries in the backend.
|
- [ ] Implement real ST_DWithin() PostGIS queries in the backend.
|
||||||
- [ ] Import full Natural Earth/GeoNames datasets into the `cities` table.
|
- [ ] Import full Natural Earth/GeoNames datasets into the `cities` table.
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ docker-compose up --build
|
|||||||
|
|
||||||
### 3. Access Application
|
### 3. Access Application
|
||||||
|
|
||||||
- **Frontend**: http://localhost:3000
|
- **Frontend**: http://localhost:3050
|
||||||
- **Backend API**: http://localhost:3001
|
- **Backend API**: http://localhost:3051
|
||||||
- **Database**: localhost:5432 (PostgreSQL with PostGIS)
|
- **Database**: localhost:5432 (PostgreSQL with PostGIS)
|
||||||
|
|
||||||
## 🎮 How to Use
|
## 🎮 How to Use
|
||||||
@@ -219,8 +219,8 @@ npm run dev
|
|||||||
### Port Already in Use
|
### Port Already in Use
|
||||||
```bash
|
```bash
|
||||||
# Find process using port
|
# Find process using port
|
||||||
lsof -i :3000
|
lsof -i :3050
|
||||||
lsof -i :3001
|
lsof -i :3051
|
||||||
|
|
||||||
# Kill process
|
# Kill process
|
||||||
kill -9 <PID>
|
kill -9 <PID>
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 3001
|
EXPOSE 3051
|
||||||
|
|
||||||
# Start server
|
# Start server
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|||||||
+77
-28
@@ -4,9 +4,8 @@ const { Pool } = require('pg');
|
|||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.PORT || 3001;
|
const PORT = process.env.PORT || 3051;
|
||||||
|
|
||||||
// Database connection pool
|
|
||||||
const pool = new Pool({
|
const pool = new Pool({
|
||||||
connectionString: process.env.DATABASE_URL || 'postgresql://line_of_sight:line_of_sight_pass@postgres:5432/line_of_sight'
|
connectionString: process.env.DATABASE_URL || 'postgresql://line_of_sight:line_of_sight_pass@postgres:5432/line_of_sight'
|
||||||
});
|
});
|
||||||
@@ -14,7 +13,6 @@ const pool = new Pool({
|
|||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
// Helper to calculate destination point given start, bearing, and distance (km)
|
|
||||||
const calculateDestination = (lat, lon, bearing, distance) => {
|
const calculateDestination = (lat, lon, bearing, distance) => {
|
||||||
const R = 6371;
|
const R = 6371;
|
||||||
const brng = (bearing * Math.PI) / 180;
|
const brng = (bearing * Math.PI) / 180;
|
||||||
@@ -39,7 +37,15 @@ const calculateDestination = (lat, lon, bearing, distance) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// Real API endpoint - uses PostGIS for spatial queries
|
const haversineKm = (lat1, lon1, lat2, lon2) => {
|
||||||
|
const R = 6371;
|
||||||
|
const toRad = (d) => d * Math.PI / 180;
|
||||||
|
const dLat = toRad(lat2 - lat1);
|
||||||
|
const dLon = toRad(lon2 - lon1);
|
||||||
|
const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2;
|
||||||
|
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||||||
|
};
|
||||||
|
|
||||||
app.get('/api/line-of-sight', async (req, res) => {
|
app.get('/api/line-of-sight', async (req, res) => {
|
||||||
const { lat, lon, direction, tolerance } = req.query;
|
const { lat, lon, direction, tolerance } = req.query;
|
||||||
|
|
||||||
@@ -48,32 +54,41 @@ app.get('/api/line-of-sight', async (req, res) => {
|
|||||||
const bearing = parseInt(direction) || 0;
|
const bearing = parseInt(direction) || 0;
|
||||||
const toleranceKm = parseInt(tolerance) || 50;
|
const toleranceKm = parseInt(tolerance) || 50;
|
||||||
|
|
||||||
console.log(`Processing real request: lat=${startLat}, lon=${startLon}, bearing=${bearing}, tolerance=${toleranceKm}`);
|
console.log(`Processing request: lat=${startLat}, lon=${startLon}, bearing=${bearing}, tolerance=${toleranceKm}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate path points for visualization and spatial query
|
|
||||||
const pathPoints = [];
|
const pathPoints = [];
|
||||||
const totalDistance = 20000;
|
const totalDistance = 40074;
|
||||||
const steps = 80; // More steps for smoother speed transition
|
const steps = 160;
|
||||||
|
|
||||||
for (let i = 0; i <= steps; i++) {
|
for (let i = 0; i <= steps; i++) {
|
||||||
const dist = (totalDistance * i) / steps;
|
const dist = (totalDistance * i) / steps;
|
||||||
pathPoints.push(calculateDestination(startLat, startLon, bearing, dist));
|
pathPoints.push(calculateDestination(startLat, startLon, bearing, dist));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch check for 'over water' status for all path points
|
// Bulk land check: use a single query for all points to avoid connection pool exhaustion
|
||||||
// We'll consider a point 'over water' if no city is within 500km
|
const waterStart = Date.now();
|
||||||
const waterChecks = await Promise.all(pathPoints.map(async (p) => {
|
const lons = pathPoints.map(p => p.lon);
|
||||||
const checkQuery = `
|
const lats = pathPoints.map(p => p.lat);
|
||||||
SELECT EXISTS (
|
|
||||||
|
const waterCheckQuery = `
|
||||||
|
WITH points AS (
|
||||||
|
SELECT i, ST_SetSRID(ST_MakePoint(lon, lat), 4326)::geography as p_geom
|
||||||
|
FROM unnest($1::float8[], $2::float8[]) WITH ORDINALITY AS t(lon, lat, 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 res = await pool.query(checkQuery, [p.lon, p.lat]);
|
|
||||||
return !res.rows[0].has_land;
|
const waterResults = await pool.query(waterCheckQuery, [lons, lats]);
|
||||||
}));
|
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,
|
||||||
@@ -82,28 +97,58 @@ app.get('/api/line-of-sight', async (req, res) => {
|
|||||||
|
|
||||||
const lineWKT = `LINESTRING(${pathPoints.map(p => `${p.lon} ${p.lat}`).join(',')})`;
|
const lineWKT = `LINESTRING(${pathPoints.map(p => `${p.lon} ${p.lat}`).join(',')})`;
|
||||||
|
|
||||||
|
// Top 5 cities per 100 km bin, ranked by population descending
|
||||||
const query = `
|
const query = `
|
||||||
WITH path AS (
|
WITH path AS (
|
||||||
SELECT ST_GeogFromText($1) as route,
|
SELECT ST_GeogFromText($1) as route,
|
||||||
ST_MakePoint($3, $4)::geography as start_node
|
ST_MakePoint($3, $4)::geography as start_node
|
||||||
)
|
),
|
||||||
|
candidates AS (
|
||||||
SELECT
|
SELECT
|
||||||
id,
|
id, name, population, country,
|
||||||
name,
|
|
||||||
population,
|
|
||||||
country,
|
|
||||||
ST_Y(geom::geometry) as lat,
|
ST_Y(geom::geometry) as lat,
|
||||||
ST_X(geom::geometry) as lon,
|
ST_X(geom::geometry) as lon,
|
||||||
ST_Distance(geom, (SELECT route FROM path)) / 1000 as distance_off_line_km,
|
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_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
|
ST_LineLocatePoint((SELECT route FROM path)::geography::geometry, geom::geometry) as pos_on_line,
|
||||||
|
FLOOR(ST_LineLocatePoint((SELECT route FROM path)::geography::geometry, geom::geometry) * 200)::int as bin_200km
|
||||||
FROM cities
|
FROM cities
|
||||||
WHERE ST_DWithin(geom, (SELECT route FROM path), $2 * 1000)
|
WHERE ST_DWithin(geom, (SELECT route FROM path), $2 * 1000)
|
||||||
ORDER BY pos_on_line ASC
|
),
|
||||||
LIMIT 200;
|
ranked AS (
|
||||||
|
SELECT *,
|
||||||
|
ROW_NUMBER() OVER (PARTITION BY bin_200km ORDER BY population DESC NULLS LAST) as rank_in_bin
|
||||||
|
FROM candidates
|
||||||
|
)
|
||||||
|
SELECT id, name, population, country, lat, lon,
|
||||||
|
distance_off_line_km, distance_from_start_km, pos_on_line
|
||||||
|
FROM ranked
|
||||||
|
WHERE rank_in_bin <= 10
|
||||||
|
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.`);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
const cityPop = city.population || 0;
|
||||||
|
const tooClose = accepted.some(s => {
|
||||||
|
const dist = haversineKm(s.lat, s.lon, city.lat, city.lon);
|
||||||
|
const sPop = s.population || 0;
|
||||||
|
if (cityPop > 1000000 && sPop > 1000000) return dist < 30;
|
||||||
|
if (cityPop > 100000 || sPop > 100000) return dist < 50;
|
||||||
|
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({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -111,7 +156,7 @@ app.get('/api/line-of-sight', async (req, res) => {
|
|||||||
start_point: { lat: startLat, lon: startLon },
|
start_point: { lat: startLat, lon: startLon },
|
||||||
direction: bearing,
|
direction: bearing,
|
||||||
tolerance_km: toleranceKm,
|
tolerance_km: toleranceKm,
|
||||||
conurbations: result.rows.map(row => ({
|
conurbations: accepted.map(row => ({
|
||||||
...row,
|
...row,
|
||||||
name: row.name || 'Unknown',
|
name: row.name || 'Unknown',
|
||||||
country: row.country || 'Unknown',
|
country: row.country || 'Unknown',
|
||||||
@@ -121,17 +166,21 @@ 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' });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (require.main === module) {
|
||||||
app.listen(PORT, '0.0.0.0', () => {
|
app.listen(PORT, '0.0.0.0', () => {
|
||||||
console.log(`Line of Sight Backend running on port ${PORT}`);
|
console.log(`Line of Sight Backend running on port ${PORT}`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { app, calculateDestination };
|
||||||
|
|||||||
Generated
+132
-1
@@ -16,7 +16,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^30.3.0",
|
"jest": "^30.3.0",
|
||||||
"nodemon": "^3.1.14"
|
"nodemon": "^3.1.14",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -932,6 +933,27 @@
|
|||||||
"@tybys/wasm-util": "^0.10.0"
|
"@tybys/wasm-util": "^0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@noble/hashes": {
|
||||||
|
"version": "1.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
|
||||||
|
"integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@paralleldrive/cuid2": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@paralleldrive/cuid2/-/cuid2-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-XO7cAxhnTZl0Yggq6jOgjiOHhbgcO4NqFqwSmQpjK3b6TEE6Uj/jfSk6wzYyemh3+I0sHirKSetjQwn5cZktFw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@noble/hashes": "^1.1.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@pkgjs/parseargs": {
|
"node_modules/@pkgjs/parseargs": {
|
||||||
"version": "0.11.0",
|
"version": "0.11.0",
|
||||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||||
@@ -1427,6 +1449,12 @@
|
|||||||
"sprintf-js": "~1.0.2"
|
"sprintf-js": "~1.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/asap": {
|
||||||
|
"version": "2.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||||
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/asynckit": {
|
"node_modules/asynckit": {
|
||||||
"version": "0.4.0",
|
"version": "0.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
@@ -1921,6 +1949,15 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/component-emitter": {
|
||||||
|
"version": "1.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz",
|
||||||
|
"integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/concat-map": {
|
"node_modules/concat-map": {
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
@@ -1969,6 +2006,12 @@
|
|||||||
"node": ">=6.6.0"
|
"node": ">=6.6.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cookiejar": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/cors": {
|
"node_modules/cors": {
|
||||||
"version": "2.8.6",
|
"version": "2.8.6",
|
||||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||||
@@ -2064,6 +2107,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dezalgo": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"asap": "^2.0.0",
|
||||||
|
"wrappy": "1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "17.3.1",
|
"version": "17.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz",
|
||||||
@@ -2329,6 +2382,12 @@
|
|||||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-safe-stringify": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/fb-watchman": {
|
"node_modules/fb-watchman": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz",
|
||||||
@@ -2456,6 +2515,23 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/formidable": {
|
||||||
|
"version": "3.5.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
|
||||||
|
"integrity": "sha512-YikH+7CUTOtP44ZTnUhR7Ic2UASBPOqmaRkRKxRbywPTe5VxF7RRCck4af9wutiZ/QKM5nME9Bie2fFaPz5Gug==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
|
"dezalgo": "^1.0.4",
|
||||||
|
"once": "^1.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://ko-fi.com/tunnckoCore/commissions"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/forwarded": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
@@ -3677,6 +3753,27 @@
|
|||||||
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
"integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/methods": {
|
||||||
|
"version": "1.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||||
|
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime": {
|
||||||
|
"version": "2.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||||
|
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"mime": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.54.0",
|
"version": "1.54.0",
|
||||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||||
@@ -4832,6 +4929,40 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/superagent": {
|
||||||
|
"version": "10.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/superagent/-/superagent-10.3.0.tgz",
|
||||||
|
"integrity": "sha512-B+4Ik7ROgVKrQsXTV0Jwp2u+PXYLSlqtDAhYnkkD+zn3yg8s/zjA2MeGayPoY/KICrbitwneDHrjSotxKL+0XQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"component-emitter": "^1.3.1",
|
||||||
|
"cookiejar": "^2.1.4",
|
||||||
|
"debug": "^4.3.7",
|
||||||
|
"fast-safe-stringify": "^2.1.1",
|
||||||
|
"form-data": "^4.0.5",
|
||||||
|
"formidable": "^3.5.4",
|
||||||
|
"methods": "^1.1.2",
|
||||||
|
"mime": "2.6.0",
|
||||||
|
"qs": "^6.14.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/supertest": {
|
||||||
|
"version": "7.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/supertest/-/supertest-7.2.2.tgz",
|
||||||
|
"integrity": "sha512-oK8WG9diS3DlhdUkcFn4tkNIiIbBx9lI2ClF8K+b2/m8Eyv47LSawxUzZQSNKUrVb2KsqeTDCcjAAVPYaSLVTA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"cookie-signature": "^1.2.2",
|
||||||
|
"methods": "^1.1.2",
|
||||||
|
"superagent": "^10.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jest": "^30.3.0",
|
"jest": "^30.3.0",
|
||||||
"nodemon": "^3.1.14"
|
"nodemon": "^3.1.14",
|
||||||
|
"supertest": "^7.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,74 @@
|
|||||||
/**
|
const request = require('supertest');
|
||||||
* Placeholder test file for Line of Sight Backend
|
const { app, calculateDestination } = require('../app/server');
|
||||||
*
|
|
||||||
* TODO: Add real tests for:
|
// Mock pg Pool
|
||||||
* - API endpoint validation
|
jest.mock('pg', () => {
|
||||||
* - Geospatial calculations
|
const mPool = {
|
||||||
* - Database queries
|
query: jest.fn(),
|
||||||
* - Error handling
|
on: jest.fn(),
|
||||||
*/
|
end: jest.fn(),
|
||||||
|
};
|
||||||
|
return { Pool: jest.fn(() => mPool) };
|
||||||
|
});
|
||||||
|
|
||||||
|
const { Pool } = require('pg');
|
||||||
|
const pool = new Pool();
|
||||||
|
|
||||||
describe('Line of Sight Backend', () => {
|
describe('Line of Sight Backend', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('calculateDestination', () => {
|
||||||
|
test('should calculate correct destination for North (0 degrees)', () => {
|
||||||
|
const start = { lat: 0, lon: 0 };
|
||||||
|
const dest = calculateDestination(start.lat, start.lon, 0, 111.12); // ~1 degree North
|
||||||
|
expect(dest.lat).toBeCloseTo(1, 1);
|
||||||
|
expect(dest.lon).toBeCloseTo(0, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should calculate correct destination for East (90 degrees)', () => {
|
||||||
|
const start = { lat: 0, lon: 0 };
|
||||||
|
const dest = calculateDestination(start.lat, start.lon, 90, 111.12); // ~1 degree East at equator
|
||||||
|
expect(dest.lat).toBeCloseTo(0, 1);
|
||||||
|
expect(dest.lon).toBeCloseTo(1, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET /api/health', () => {
|
describe('GET /api/health', () => {
|
||||||
test('should return 200 OK', () => {
|
test('should return 200 OK', async () => {
|
||||||
// TODO: Implement health check test
|
const response = await request(app).get('/api/health');
|
||||||
expect(true).toBe(true);
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.status).toBe('ok');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /api/line-of-sight', () => {
|
describe('GET /api/line-of-sight', () => {
|
||||||
test('should return valid response structure', () => {
|
test('should return valid response structure', async () => {
|
||||||
// TODO: Implement line of sight API test
|
// Mock the water checks and the main city query
|
||||||
expect(true).toBe(true);
|
pool.query
|
||||||
|
.mockResolvedValueOnce({ rows: [{ has_land: true }] }) // Simplified for tests, it actually calls many times
|
||||||
|
.mockResolvedValue({ rows: [{ id: 1, name: 'London', population: 9000000, country: 'GB', lat: 51.5, lon: -0.1, distance_off_line_km: 0, distance_from_start_km: 0, pos_on_line: 0 }] });
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/api/line-of-sight')
|
||||||
|
.query({ lat: 51.5, lon: -0.1, direction: 45, tolerance: 50 });
|
||||||
|
|
||||||
|
expect(response.status).toBe(200);
|
||||||
|
expect(response.body.success).toBe(true);
|
||||||
|
expect(response.body.data.conurbations).toBeDefined();
|
||||||
|
expect(response.body.data.line_coordinates).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle missing parameters', () => {
|
test('should handle database errors', async () => {
|
||||||
// TODO: Implement error handling test
|
pool.query.mockRejectedValue(new Error('DB Error'));
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Geospatial Calculations', () => {
|
const response = await request(app)
|
||||||
test('should calculate great circle path', () => {
|
.get('/api/line-of-sight')
|
||||||
// TODO: Implement geospatial calculation tests
|
.query({ lat: 51.5, lon: -0.1, direction: 45, tolerance: 50 });
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should filter cities within tolerance', () => {
|
expect(response.status).toBe(500);
|
||||||
// TODO: Implement tolerance filtering tests
|
expect(response.body.success).toBe(false);
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-11
@@ -1,17 +1,11 @@
|
|||||||
version: '3.8'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: kartoza/postgis:latest
|
image: kartoza/postgis:latest
|
||||||
platform: linux/arm64
|
|
||||||
container_name: line-of-sight-db
|
container_name: line-of-sight-db
|
||||||
environment:
|
environment:
|
||||||
- POSTGRES_DB=line_of_sight
|
- POSTGRES_DB=line_of_sight
|
||||||
- POSTGRES_USER=line_of_sight
|
- POSTGRES_USER=line_of_sight
|
||||||
- POSTGRES_PASS=line_of_sight_pass
|
- POSTGRES_PASS=line_of_sight_pass
|
||||||
- ALLOW_IP_RANGE=0.0.0.0/0
|
|
||||||
ports:
|
|
||||||
- "5432:5432"
|
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql
|
- pgdata:/var/lib/postgresql
|
||||||
- ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
|
- ./docker/init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||||
@@ -26,11 +20,12 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: line-of-sight-backend
|
container_name: line-of-sight-backend
|
||||||
ports:
|
# Removed public port exposure for security; accessible via frontend proxy
|
||||||
- "3001:3001"
|
# ports:
|
||||||
|
# - "3051:3051"
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://line_of_sight:line_of_sight_pass@postgres:5432/line_of_sight
|
- DATABASE_URL=postgresql://line_of_sight:line_of_sight_pass@postgres:5432/line_of_sight
|
||||||
- PORT=3001
|
- PORT=3051
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -44,9 +39,9 @@ services:
|
|||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: line-of-sight-frontend
|
container_name: line-of-sight-frontend
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3050:3050"
|
||||||
environment:
|
environment:
|
||||||
- VITE_API_URL=http://localhost:3001/api
|
- VITE_API_URL=/api
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Binary file not shown.
+3
-3
@@ -10,10 +10,10 @@ RUN npm install
|
|||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Expose port
|
# Expose port
|
||||||
EXPOSE 3000
|
EXPOSE 3050
|
||||||
|
|
||||||
# Set environment variable for API URL
|
# Set environment variable for API URL (relative to the frontend host)
|
||||||
ENV VITE_API_URL=http://localhost:3001/api
|
ENV VITE_API_URL=/api
|
||||||
|
|
||||||
# Start development server
|
# Start development server
|
||||||
CMD ["npm", "start"]
|
CMD ["npm", "start"]
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
// MapLibre GL requires WebGL which isn't available in headless Chromium.
|
||||||
|
// Intercept the module request and return a lightweight mock so React can render.
|
||||||
|
const MAPLIBRE_MOCK = `
|
||||||
|
class Map {
|
||||||
|
constructor(opts) {
|
||||||
|
this._listeners = {};
|
||||||
|
Promise.resolve().then(() => this._emit('style.load'));
|
||||||
|
}
|
||||||
|
_emit(event) {
|
||||||
|
(this._listeners[event] || []).forEach(fn => fn());
|
||||||
|
}
|
||||||
|
on(event, fn) {
|
||||||
|
if (!this._listeners[event]) this._listeners[event] = [];
|
||||||
|
this._listeners[event].push(fn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
off(event, fn) {
|
||||||
|
this._listeners[event] = (this._listeners[event] || []).filter(l => l !== fn);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
remove() {}
|
||||||
|
getSource() { return null; }
|
||||||
|
getLayer() { return null; }
|
||||||
|
addSource() {}
|
||||||
|
addLayer() {}
|
||||||
|
removeLayer() {}
|
||||||
|
removeSource() {}
|
||||||
|
setSky() {}
|
||||||
|
setTerrain() {}
|
||||||
|
setLayoutProperty() {}
|
||||||
|
flyTo() {}
|
||||||
|
jumpTo() {}
|
||||||
|
setStyle() { Promise.resolve().then(() => this._emit('style.load')); }
|
||||||
|
setProjection() {}
|
||||||
|
hasImage() { return false; }
|
||||||
|
addImage() {}
|
||||||
|
getCanvas() { return document.createElement('canvas'); }
|
||||||
|
project() { return { x: 0, y: 0 }; }
|
||||||
|
unproject() { return { lng: 0, lat: 0 }; }
|
||||||
|
}
|
||||||
|
class Marker {
|
||||||
|
constructor(opts) {
|
||||||
|
this._el = (opts && opts.element) || document.createElement('div');
|
||||||
|
}
|
||||||
|
setLngLat() { return this; }
|
||||||
|
addTo() { return this; }
|
||||||
|
remove() {}
|
||||||
|
getLngLat() { return { lng: 0, lat: 0 }; }
|
||||||
|
getElement() { return this._el; }
|
||||||
|
}
|
||||||
|
class Popup {
|
||||||
|
setLngLat() { return this; }
|
||||||
|
setDOMContent() { return this; }
|
||||||
|
addTo() { return this; }
|
||||||
|
remove() {}
|
||||||
|
}
|
||||||
|
class LngLatBounds {
|
||||||
|
extend() { return this; }
|
||||||
|
}
|
||||||
|
export default { Map, Marker, Popup, LngLatBounds };
|
||||||
|
`;
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await page.route('**maplibre-gl.js*', async route => {
|
||||||
|
await route.fulfill({ contentType: 'application/javascript', body: MAPLIBRE_MOCK });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('Line of Sight Application', () => {
|
||||||
|
test('should load the home page and show settings', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await expect(page.locator('text=Line of Sight Settings')).toBeVisible();
|
||||||
|
await expect(page.locator('label:has-text("Direction")')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be able to toggle map style', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForSelector('h3:text("Line of Sight Settings")', { timeout: 10000 });
|
||||||
|
const darkButton = page.locator('button:text("Dark")');
|
||||||
|
await darkButton.click();
|
||||||
|
await expect(darkButton).toHaveClass(/active-style/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should show results when clicking the search button', async ({ page }) => {
|
||||||
|
await page.goto('/');
|
||||||
|
await page.waitForSelector('h3:text("Line of Sight Settings")', { timeout: 10000 });
|
||||||
|
|
||||||
|
await page.route('**/api/line-of-sight*', async route => {
|
||||||
|
const json = {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
conurbations: [
|
||||||
|
{ id: 1, name: 'Test City', population: 100000, country: 'TS', lat: 0, lon: 0, distance_km: 10, off_line_km: 1 }
|
||||||
|
],
|
||||||
|
line_coordinates: [{ lat: 0, lon: 0 }, { lat: 1, lon: 1 }]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await route.fulfill({ json });
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.click('button:text("Show Line of Sight")');
|
||||||
|
await expect(page.locator('text=Conurbations Found')).toBeVisible();
|
||||||
|
await expect(page.locator('text=Test City')).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
Generated
+235
-8
@@ -15,10 +15,11 @@
|
|||||||
"react-dom": "^19.2.1"
|
"react-dom": "^19.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@testing-library/jest-dom": "^6.6.0",
|
"@testing-library/jest-dom": "^6.6.0",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"jsdom": "^29.0.0",
|
"happy-dom": "^20.8.4",
|
||||||
"vite": "^6.0.0",
|
"vite": "^6.0.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
}
|
}
|
||||||
@@ -34,6 +35,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.0.1.tgz",
|
||||||
"integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
|
"integrity": "sha512-2SZFvqMyvboVV1d15lMf7XiI3m7SDqXUuKaTymJYLN6dSGadqp+fVojqJlVoMlbZnlTmu3S0TLwLTJpvBMO1Aw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/css-calc": "^3.1.1",
|
"@csstools/css-calc": "^3.1.1",
|
||||||
"@csstools/css-color-parser": "^4.0.2",
|
"@csstools/css-color-parser": "^4.0.2",
|
||||||
@@ -50,6 +53,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
@@ -59,6 +64,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.0.3.tgz",
|
||||||
"integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==",
|
"integrity": "sha512-Q6mU0Z6bfj6YvnX2k9n0JxiIwrCFN59x/nWmYQnAqP000ruX/yV+5bp/GRcF5T8ncvfwJQ7fgfP74DlpKExILA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asamuzakjp/nwsapi": "^2.3.9",
|
"@asamuzakjp/nwsapi": "^2.3.9",
|
||||||
"bidi-js": "^1.0.3",
|
"bidi-js": "^1.0.3",
|
||||||
@@ -75,6 +82,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
@@ -83,7 +92,9 @@
|
|||||||
"version": "2.3.9",
|
"version": "2.3.9",
|
||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz",
|
||||||
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
"integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
@@ -362,6 +373,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
|
||||||
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
"integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"css-tree": "^3.0.0"
|
"css-tree": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -384,6 +397,8 @@
|
|||||||
"url": "https://opencollective.com/csstools"
|
"url": "https://opencollective.com/csstools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
@@ -403,6 +418,8 @@
|
|||||||
"url": "https://opencollective.com/csstools"
|
"url": "https://opencollective.com/csstools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
@@ -426,6 +443,8 @@
|
|||||||
"url": "https://opencollective.com/csstools"
|
"url": "https://opencollective.com/csstools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@csstools/color-helpers": "^6.0.2",
|
"@csstools/color-helpers": "^6.0.2",
|
||||||
"@csstools/css-calc": "^3.1.1"
|
"@csstools/css-calc": "^3.1.1"
|
||||||
@@ -453,6 +472,8 @@
|
|||||||
"url": "https://opencollective.com/csstools"
|
"url": "https://opencollective.com/csstools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
},
|
},
|
||||||
@@ -475,6 +496,8 @@
|
|||||||
"url": "https://opencollective.com/csstools"
|
"url": "https://opencollective.com/csstools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"css-tree": "^3.2.1"
|
"css-tree": "^3.2.1"
|
||||||
},
|
},
|
||||||
@@ -499,6 +522,8 @@
|
|||||||
"url": "https://opencollective.com/csstools"
|
"url": "https://opencollective.com/csstools"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.19.0"
|
"node": ">=20.19.0"
|
||||||
}
|
}
|
||||||
@@ -924,6 +949,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz",
|
||||||
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
|
"integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
@@ -1076,6 +1103,21 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@maplibre/geojson-vt/-/geojson-vt-5.0.4.tgz",
|
||||||
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ=="
|
"integrity": "sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rolldown/pluginutils": {
|
"node_modules/@rolldown/pluginutils": {
|
||||||
"version": "1.0.0-beta.27",
|
"version": "1.0.0-beta.27",
|
||||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
|
||||||
@@ -3508,6 +3550,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/kdbush/-/kdbush-3.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/kdbush/-/kdbush-3.0.5.tgz",
|
||||||
"integrity": "sha512-tdJz7jaWFu4nR+8b2B+CdPZ6811ighYylWsu2hpsivapzW058yP0KdfZuNY89IiRe5jbKvBGXN3LQdN2KPXVdQ=="
|
"integrity": "sha512-tdJz7jaWFu4nR+8b2B+CdPZ6811ighYylWsu2hpsivapzW058yP0KdfZuNY89IiRe5jbKvBGXN3LQdN2KPXVdQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "25.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz",
|
||||||
|
"integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~7.18.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/supercluster": {
|
"node_modules/@types/supercluster": {
|
||||||
"version": "7.1.3",
|
"version": "7.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
|
||||||
@@ -3516,6 +3567,21 @@
|
|||||||
"@types/geojson": "*"
|
"@types/geojson": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/whatwg-mimetype": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
|
"node_modules/@types/ws": {
|
||||||
|
"version": "8.18.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||||
|
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@vitejs/plugin-react": {
|
"node_modules/@vitejs/plugin-react": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
|
||||||
@@ -3725,6 +3791,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
|
||||||
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
"integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"require-from-string": "^2.0.2"
|
"require-from-string": "^2.0.2"
|
||||||
}
|
}
|
||||||
@@ -3879,6 +3947,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz",
|
||||||
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
"integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mdn-data": "2.27.1",
|
"mdn-data": "2.27.1",
|
||||||
"source-map-js": "^1.2.1"
|
"source-map-js": "^1.2.1"
|
||||||
@@ -3916,6 +3986,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz",
|
||||||
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
"integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"whatwg-mimetype": "^5.0.0",
|
"whatwg-mimetype": "^5.0.0",
|
||||||
"whatwg-url": "^16.0.0"
|
"whatwg-url": "^16.0.0"
|
||||||
@@ -3945,7 +4017,9 @@
|
|||||||
"version": "10.6.0",
|
"version": "10.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/deep-eql": {
|
"node_modules/deep-eql": {
|
||||||
"version": "5.0.2",
|
"version": "5.0.2",
|
||||||
@@ -4009,6 +4083,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
},
|
},
|
||||||
@@ -4311,6 +4387,44 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/happy-dom": {
|
||||||
|
"version": "20.8.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.8.4.tgz",
|
||||||
|
"integrity": "sha512-GKhjq4OQCYB4VLFBzv8mmccUadwlAusOZOI7hC1D9xDIT5HhzkJK17c4el2f6R6C715P9xB4uiMxeKUa2nHMwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": ">=20.0.0",
|
||||||
|
"@types/whatwg-mimetype": "^3.0.2",
|
||||||
|
"@types/ws": "^8.18.1",
|
||||||
|
"entities": "^7.0.1",
|
||||||
|
"whatwg-mimetype": "^3.0.0",
|
||||||
|
"ws": "^8.18.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/happy-dom/node_modules/entities": {
|
||||||
|
"version": "7.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||||
|
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/happy-dom/node_modules/whatwg-mimetype": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/has-symbols": {
|
"node_modules/has-symbols": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||||
@@ -4352,6 +4466,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz",
|
||||||
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
"integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@exodus/bytes": "^1.6.0"
|
"@exodus/bytes": "^1.6.0"
|
||||||
},
|
},
|
||||||
@@ -4372,7 +4488,9 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/js-tokens": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
@@ -4385,6 +4503,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.0.0.tgz",
|
||||||
"integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==",
|
"integrity": "sha512-9FshNB6OepopZ08unmmGpsF7/qCjxGPbo3NbgfJAnPeHXnsODE9WWffXZtRFRFe0ntzaAOcSKNJFz8wiyvF1jQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asamuzakjp/css-color": "^5.0.1",
|
"@asamuzakjp/css-color": "^5.0.1",
|
||||||
"@asamuzakjp/dom-selector": "^7.0.2",
|
"@asamuzakjp/dom-selector": "^7.0.2",
|
||||||
@@ -4425,6 +4545,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz",
|
||||||
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
"integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "20 || >=22"
|
"node": "20 || >=22"
|
||||||
}
|
}
|
||||||
@@ -4550,7 +4672,9 @@
|
|||||||
"version": "2.27.1",
|
"version": "2.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz",
|
||||||
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
"integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/mime-db": {
|
"node_modules/mime-db": {
|
||||||
"version": "1.52.0",
|
"version": "1.52.0",
|
||||||
@@ -4628,6 +4752,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz",
|
||||||
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
"integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"entities": "^6.0.0"
|
"entities": "^6.0.0"
|
||||||
},
|
},
|
||||||
@@ -4679,6 +4805,50 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.58.2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.58.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||||
|
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||||
|
"dev": true,
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright/node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/point-in-polygon": {
|
"node_modules/point-in-polygon": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/point-in-polygon/-/point-in-polygon-1.1.0.tgz",
|
||||||
@@ -4769,6 +4939,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
@@ -4844,6 +5016,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||||
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -4915,6 +5089,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"xmlchars": "^2.2.0"
|
"xmlchars": "^2.2.0"
|
||||||
},
|
},
|
||||||
@@ -5028,7 +5204,9 @@
|
|||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
@@ -5095,6 +5273,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz",
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.26.tgz",
|
||||||
"integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==",
|
"integrity": "sha512-WiGwQjr0qYdNNG8KpMKlSvpxz652lqa3Rd+/hSaDcY4Uo6SKWZq2LAF+hsAhUewTtYhXlorBKgNF3Kk8hnjGoQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tldts-core": "^7.0.26"
|
"tldts-core": "^7.0.26"
|
||||||
},
|
},
|
||||||
@@ -5106,7 +5286,9 @@
|
|||||||
"version": "7.0.26",
|
"version": "7.0.26",
|
||||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz",
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.26.tgz",
|
||||||
"integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==",
|
"integrity": "sha512-5WJ2SqFsv4G2Dwi7ZFVRnz6b2H1od39QME1lc2y5Ew3eWiZMAeqOAfWpRP9jHvhUl881406QtZTODvjttJs+ew==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/topojson-client": {
|
"node_modules/topojson-client": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
@@ -5137,6 +5319,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
|
||||||
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
"integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tldts": "^7.0.5"
|
"tldts": "^7.0.5"
|
||||||
},
|
},
|
||||||
@@ -5149,6 +5333,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz",
|
||||||
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
"integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"punycode": "^2.3.1"
|
"punycode": "^2.3.1"
|
||||||
},
|
},
|
||||||
@@ -5166,10 +5352,18 @@
|
|||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz",
|
||||||
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
|
"integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.18.1"
|
"node": ">=20.18.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||||
|
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
||||||
@@ -5373,6 +5567,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"xml-name-validator": "^5.0.0"
|
"xml-name-validator": "^5.0.0"
|
||||||
},
|
},
|
||||||
@@ -5385,6 +5581,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz",
|
||||||
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
"integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
@@ -5394,6 +5592,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
|
||||||
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
"integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
@@ -5403,6 +5603,8 @@
|
|||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz",
|
||||||
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
"integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@exodus/bytes": "^1.11.0",
|
"@exodus/bytes": "^1.11.0",
|
||||||
"tr46": "^6.0.0",
|
"tr46": "^6.0.0",
|
||||||
@@ -5428,11 +5630,34 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.19.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||||
|
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||||
|
"dev": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -5441,7 +5666,9 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||||
"dev": true
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/yallist": {
|
"node_modules/yallist": {
|
||||||
"version": "3.1.1",
|
"version": "3.1.1",
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
"react-dom": "^19.2.1"
|
"react-dom": "^19.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.58.2",
|
||||||
"@testing-library/jest-dom": "^6.6.0",
|
"@testing-library/jest-dom": "^6.6.0",
|
||||||
"@testing-library/react": "^16.1.0",
|
"@testing-library/react": "^16.1.0",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"jsdom": "^29.0.0",
|
"happy-dom": "^20.8.4",
|
||||||
"vite": "^6.0.0",
|
"vite": "^6.0.0",
|
||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
@@ -21,7 +22,8 @@
|
|||||||
"start": "vite",
|
"start": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest"
|
"test": "vitest",
|
||||||
|
"test:e2e": "playwright test"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,34 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: process.env.CI ? 'list' : 'html',
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3050',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
headless: true,
|
||||||
|
launchOptions: {
|
||||||
|
args: [
|
||||||
|
'--enable-webgl',
|
||||||
|
'--ignore-gpu-blocklist',
|
||||||
|
'--use-gl=angle',
|
||||||
|
'--use-angle=swiftshader-webgl',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webServer: {
|
||||||
|
command: 'npm run start',
|
||||||
|
url: 'http://localhost:3050',
|
||||||
|
reuseExistingServer: !process.env.CI,
|
||||||
|
},
|
||||||
|
});
|
||||||
+488
-218
File diff suppressed because it is too large
Load Diff
@@ -1,53 +1,112 @@
|
|||||||
/**
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||||
* Placeholder test file for Line of Sight Frontend
|
import { vi, describe, test, expect, beforeEach } from 'vitest';
|
||||||
*
|
import App from '../App';
|
||||||
* TODO: Add real tests for:
|
import apiService from '../services/api';
|
||||||
* - Map component rendering
|
|
||||||
* - Direction selector functionality
|
// Mock MapLibre GL
|
||||||
* - API integration
|
vi.mock('maplibre-gl', () => {
|
||||||
* - User interactions
|
const mInstance = {
|
||||||
* - Line of sight visualization
|
on: vi.fn(),
|
||||||
*/
|
off: vi.fn(),
|
||||||
|
remove: vi.fn(),
|
||||||
|
addSource: vi.fn(),
|
||||||
|
removeSource: vi.fn(),
|
||||||
|
addLayer: vi.fn(),
|
||||||
|
removeLayer: vi.fn(),
|
||||||
|
getSource: vi.fn(() => ({ setData: vi.fn() })),
|
||||||
|
getLayer: vi.fn(),
|
||||||
|
setLayoutProperty: vi.fn(),
|
||||||
|
setStyle: vi.fn(),
|
||||||
|
flyTo: vi.fn(),
|
||||||
|
jumpTo: vi.fn(),
|
||||||
|
fitBounds: vi.fn(),
|
||||||
|
isStyleLoaded: vi.fn(() => true),
|
||||||
|
setSky: vi.fn(),
|
||||||
|
setTerrain: vi.fn(),
|
||||||
|
setProjection: vi.fn(),
|
||||||
|
hasImage: vi.fn(() => false),
|
||||||
|
addImage: vi.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
default: {
|
||||||
|
Map: vi.fn(() => mInstance),
|
||||||
|
Marker: vi.fn(() => ({
|
||||||
|
setLngLat: vi.fn().mockReturnThis(),
|
||||||
|
addTo: vi.fn().mockReturnThis(),
|
||||||
|
remove: vi.fn().mockReturnThis(),
|
||||||
|
getElement: vi.fn(() => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
// Add a simple way to trigger click for tests if needed
|
||||||
|
return el;
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
Popup: vi.fn(() => ({
|
||||||
|
setLngLat: vi.fn().mockReturnThis(),
|
||||||
|
setDOMContent: vi.fn().mockReturnThis(),
|
||||||
|
addTo: vi.fn().mockReturnThis(),
|
||||||
|
remove: vi.fn().mockReturnThis(),
|
||||||
|
on: vi.fn().mockReturnThis(),
|
||||||
|
})),
|
||||||
|
LngLatBounds: vi.fn(() => ({
|
||||||
|
extend: vi.fn().mockReturnThis(),
|
||||||
|
})),
|
||||||
|
MercatorCoordinate: {
|
||||||
|
fromLngLat: vi.fn(() => ({ x: 0, y: 0, z: 0 })),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock apiService
|
||||||
|
vi.mock('../services/api', () => ({
|
||||||
|
default: {
|
||||||
|
getLineOfSight: vi.fn(),
|
||||||
|
healthCheck: vi.fn(),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
describe('Line of Sight Frontend', () => {
|
|
||||||
describe('App Component', () => {
|
describe('App Component', () => {
|
||||||
test('should render map container', () => {
|
beforeEach(() => {
|
||||||
// TODO: Implement component rendering test
|
vi.clearAllMocks();
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should display direction selector', () => {
|
test('renders the application title', () => {
|
||||||
// TODO: Implement direction selector test
|
render(<App />);
|
||||||
expect(true).toBe(true);
|
expect(screen.getByText(/Line of Sight Settings/i)).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should handle map click events', () => {
|
test('renders initial controls', () => {
|
||||||
// TODO: Implement click event test
|
render(<App />);
|
||||||
expect(true).toBe(true);
|
// Use role or more specific text to avoid duplicates
|
||||||
|
expect(screen.getByText(/Direction \(0-360°\):/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: /Show Line of Sight/i })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('calls API when clicking "Show Line of Sight"', async () => {
|
||||||
|
apiService.getLineOfSight.mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
success: true,
|
||||||
|
data: {
|
||||||
|
conurbations: [],
|
||||||
|
line_coordinates: [{ lat: 0, lon: 0 }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<App />);
|
||||||
|
const button = screen.getByRole('button', { name: /Show Line of Sight/i });
|
||||||
|
fireEvent.click(button);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(apiService.getLineOfSight).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('API Integration', () => {
|
test('toggles map style', () => {
|
||||||
test('should fetch line of sight data', () => {
|
render(<App />);
|
||||||
// TODO: Implement API fetch test
|
const darkButton = screen.getByRole('button', { name: /Dark/i });
|
||||||
expect(true).toBe(true);
|
fireEvent.click(darkButton);
|
||||||
});
|
expect(darkButton).toHaveClass('active-style');
|
||||||
|
|
||||||
test('should handle API errors gracefully', () => {
|
|
||||||
// TODO: Implement error handling test
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('UI Components', () => {
|
|
||||||
test('should toggle map style between light/dark', () => {
|
|
||||||
// TODO: Implement style toggle test
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should display conurbation results table', () => {
|
|
||||||
// TODO: Implement results table test
|
|
||||||
expect(true).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001/api';
|
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: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getLineOfSight = async (lat, lon, direction, tolerance) => {
|
export const getLineOfSight = async (lat, lon, direction, tolerance) => {
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Global mocks if needed
|
||||||
+158
-57
@@ -13,6 +13,30 @@
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.locate-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
z-index: 10;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
background: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #333;
|
||||||
|
transition: background 0.15s, color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.locate-btn:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
color: #1a73e8;
|
||||||
|
}
|
||||||
|
|
||||||
.controls {
|
.controls {
|
||||||
width: 350px;
|
width: 350px;
|
||||||
background: white;
|
background: white;
|
||||||
@@ -176,6 +200,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
table-layout: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-panel th {
|
.results-panel th {
|
||||||
@@ -189,6 +214,13 @@
|
|||||||
padding: 8px 4px;
|
padding: 8px 4px;
|
||||||
border-bottom: 1px solid #ddd;
|
border-bottom: 1px solid #ddd;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-panel tr {
|
||||||
|
will-change: transform, background-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.results-panel tr:nth-child(even) {
|
.results-panel tr:nth-child(even) {
|
||||||
@@ -322,63 +354,6 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.city-marker {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
font-family: sans-serif;
|
|
||||||
z-index: 100;
|
|
||||||
cursor: pointer;
|
|
||||||
pointer-events: auto !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-number {
|
|
||||||
background: #2c3e50;
|
|
||||||
color: white;
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-bottom: -4px;
|
|
||||||
border: 1px solid white;
|
|
||||||
z-index: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-dot {
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
background: #e74c3c;
|
|
||||||
border: 2px solid white;
|
|
||||||
border-radius: 50%;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-label {
|
|
||||||
background: white;
|
|
||||||
padding: 4px 8px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
|
||||||
margin-top: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.marker-pop {
|
|
||||||
background: #34495e;
|
|
||||||
color: white;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 10px;
|
|
||||||
margin-top: 2px;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.map-info-popup {
|
.map-info-popup {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
@@ -411,6 +386,37 @@
|
|||||||
padding: 15px !important;
|
padding: 15px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.projection-row {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projection-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projection-buttons button {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
background: white;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projection-buttons button:hover {
|
||||||
|
background: #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.projection-buttons button.active-style {
|
||||||
|
background: #34495e;
|
||||||
|
color: white;
|
||||||
|
border-color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.app-container {
|
.app-container {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -434,3 +440,98 @@
|
|||||||
top: auto;
|
top: auto;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.results-panel tr.passed-row {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-panel tr.current-row {
|
||||||
|
background: #1abc9c !important;
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results-panel tr.upcoming-row {
|
||||||
|
background: #eaf4fb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.now-passing {
|
||||||
|
background: #1abc9c;
|
||||||
|
color: white;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-banner {
|
||||||
|
background: #fdecea;
|
||||||
|
color: #d32f2f;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 4px solid #d32f2f;
|
||||||
|
font-size: 13px;
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fly-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fly-button {
|
||||||
|
flex: 3;
|
||||||
|
padding: 12px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fly-button.play {
|
||||||
|
background: #3498db;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fly-button.play:hover {
|
||||||
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fly-button.pause {
|
||||||
|
background: #e74c3c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fly-button.pause:hover {
|
||||||
|
background: #c0392b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-flight-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 12px;
|
||||||
|
background: #95a5a6;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-flight-btn:hover:not(:disabled) {
|
||||||
|
background: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-flight-btn:disabled {
|
||||||
|
background: #ecf0f1;
|
||||||
|
color: #bdc3c7;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
+11
-2
@@ -5,11 +5,20 @@ export default defineConfig({
|
|||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
test: {
|
test: {
|
||||||
globals: true,
|
globals: true,
|
||||||
environment: 'jsdom',
|
environment: 'happy-dom',
|
||||||
|
setupFiles: ['./src/setupTests.js'],
|
||||||
|
exclude: ['**/e2e/**', '**/node_modules/**'],
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3050,
|
||||||
host: '0.0.0.0',
|
host: '0.0.0.0',
|
||||||
|
allowedHosts: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://backend:3051',
|
||||||
|
changeOrigin: true
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
outDir: 'build',
|
outDir: 'build',
|
||||||
|
|||||||
Executable
+66
@@ -0,0 +1,66 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
CONTAINER_NAME="line-of-sight-db"
|
||||||
|
DB_USER="line_of_sight"
|
||||||
|
DB_NAME="line_of_sight"
|
||||||
|
DATA_FILE="./docker/data/cities.csv.gz"
|
||||||
|
|
||||||
|
# Check if docker is available using the known path
|
||||||
|
DOCKER_BIN="/usr/local/bin/docker"
|
||||||
|
if [ ! -f "$DOCKER_BIN" ]; then
|
||||||
|
DOCKER_BIN=$(which docker)
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$DOCKER_BIN" ]; then
|
||||||
|
echo "Error: docker command not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if data file exists
|
||||||
|
if [ ! -f "$DATA_FILE" ]; then
|
||||||
|
echo "Error: Data file $DATA_FILE not found."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if container is running
|
||||||
|
if ! "$DOCKER_BIN" ps | grep -q "$CONTAINER_NAME"; then
|
||||||
|
echo "Error: Container $CONTAINER_NAME is not running."
|
||||||
|
echo "Please run 'docker-compose up -d' first."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "🚀 Starting database initialization..."
|
||||||
|
|
||||||
|
# Use a HEREDOC to execute multi-line SQL
|
||||||
|
# We load into a temp table first to handle the WKT -> Geography conversion
|
||||||
|
IMPORT_SQL=$(cat <<EOF
|
||||||
|
CREATE TEMP TABLE tmp_cities (
|
||||||
|
name VARCHAR(255),
|
||||||
|
population INTEGER,
|
||||||
|
country VARCHAR(100),
|
||||||
|
geom_wkt TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
COPY tmp_cities FROM STDIN WITH CSV HEADER;
|
||||||
|
|
||||||
|
TRUNCATE cities;
|
||||||
|
|
||||||
|
INSERT INTO cities (name, population, country, geom)
|
||||||
|
SELECT
|
||||||
|
name,
|
||||||
|
population,
|
||||||
|
country,
|
||||||
|
ST_GeomFromText(geom_wkt, 4326)::geography
|
||||||
|
FROM tmp_cities;
|
||||||
|
|
||||||
|
DROP TABLE tmp_cities;
|
||||||
|
|
||||||
|
SELECT count(*) || ' cities successfully imported.' as result FROM cities;
|
||||||
|
EOF
|
||||||
|
)
|
||||||
|
|
||||||
|
# Stream the gzipped data into psql
|
||||||
|
gunzip -c "$DATA_FILE" | "$DOCKER_BIN" exec -i -e PGPASSWORD=line_of_sight_pass "$CONTAINER_NAME" psql -U "$DB_USER" -d "$DB_NAME" -h 127.0.0.1 -c "$IMPORT_SQL"
|
||||||
|
|
||||||
|
echo "✅ Initialization complete."
|
||||||
Reference in New Issue
Block a user