Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions engine-backend/src/modules/common/geo.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Shared geospatial utility functions.
* Single source of truth for Haversine distance calculations
* across all backend modules.
*/

const EARTH_RADIUS_KM = 6371;

/**
* Computes the great-circle distance in kilometres between two
* latitude/longitude coordinates using the Haversine formula.
*/
export function haversineKm(
lat1: number,
lng1: number,
lat2: number,
lng2: number,
): number {
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2);
return EARTH_RADIUS_KM * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
14 changes: 2 additions & 12 deletions engine-backend/src/modules/risk/risk.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { resolveRouteRegion } from '../common/region-resolver.util';
import { ObservabilityService } from '../common/observability.service';
import { classifyRiskLevel } from './risk.algorithm';
import { RiskSurfaceCacheService } from './risk-surface-cache.service';
import { haversineKm } from '../common/geo.util';

@Injectable()
export class RiskService {
Expand Down Expand Up @@ -287,7 +288,7 @@ export class RiskService {
private calculatePathDistance(coords: { lat: number, lng: number }[]): number {
let dist = 0;
for (let i = 0; i < coords.length - 1; i++) {
dist += this.haversine(coords[i], coords[i + 1]);
dist += haversineKm(coords[i].lat, coords[i].lng, coords[i + 1].lat, coords[i + 1].lng);
}
return dist;
}
Expand All @@ -306,15 +307,4 @@ export class RiskService {
}
return nearest;
}

private haversine(c1: { lat: number, lng: number }, c2: { lat: number, lng: number }): number {
const R = 6371; // km
const dLat = (c2.lat - c1.lat) * Math.PI / 180;
const dLng = (c2.lng - c1.lng) * Math.PI / 180;
const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(c1.lat * Math.PI / 180) * Math.cos(c2.lat * Math.PI / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return R * c;
}
}
29 changes: 10 additions & 19 deletions engine-backend/src/modules/route-options/route-options.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { performance } from 'perf_hooks';
import { QueryResultRow } from 'pg';
import { DatabaseService } from '../../database/database.service';
import { haversineKm } from '../common/geo.util';
import { SpatialService } from '../common/spatial.service';
import {
EtaQueryDto,
Expand Down Expand Up @@ -357,7 +358,10 @@ export class RouteOptionsService {
};
}

const distanceKm = this.haversineKm(dto.origin, dto.destination);
const distanceKm = haversineKm(
dto.origin.lat, dto.origin.lng,
dto.destination.lat, dto.destination.lng,
);
const startNode = await this.findNearestNode(dto.origin, dto.city_id);
const endNode = await this.findNearestNode(dto.destination, dto.city_id);
if (!startNode || !endNode) {
Expand Down Expand Up @@ -460,7 +464,10 @@ export class RouteOptionsService {
}

private async loadGraph(origin: LatLngDto, destination: LatLngDto, cityId?: string): Promise<Graph> {
const distanceKm = this.haversineKm(origin, destination);
const distanceKm = haversineKm(
origin.lat, origin.lng,
destination.lat, destination.lng,
);
const margin = Math.max(0.05, distanceKm / 80);
const minLat = Math.min(origin.lat, destination.lat) - margin;
const maxLat = Math.max(origin.lat, destination.lat) + margin;
Expand Down Expand Up @@ -698,10 +705,7 @@ export class RouteOptionsService {
if (!node) {
return 0;
}
const distanceKm = this.haversineKm(
{ lat: node.lat, lng: node.lng },
{ lat: end.lat, lng: end.lng },
);
const distanceKm = haversineKm(node.lat, node.lng, end.lat, end.lng);
return (distanceKm * weights.distance) + ((distanceKm / 70) * 3600 * weights.travelTime);
}

Expand Down Expand Up @@ -932,19 +936,6 @@ export class RouteOptionsService {
return { from, to };
}

private haversineKm(a: { lat: number; lng: number }, b: { lat: number; lng: number }) {
const R = 6371;
const dLat = ((b.lat - a.lat) * Math.PI) / 180;
const dLng = ((b.lng - a.lng) * Math.PI) / 180;
const aa =
Math.sin(dLat / 2) ** 2 +
Math.cos((a.lat * Math.PI) / 180) *
Math.cos((b.lat * Math.PI) / 180) *
Math.sin(dLng / 2) ** 2;
const c = 2 * Math.atan2(Math.sqrt(aa), Math.sqrt(1 - aa));
return R * c;
}

private bearing(a: GraphNode, b: GraphNode) {
const lat1 = (a.lat * Math.PI) / 180;
const lat2 = (b.lat * Math.PI) / 180;
Expand Down
16 changes: 2 additions & 14 deletions engine-backend/src/modules/routing/pathfinding.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Injectable, Logger } from '@nestjs/common';
import { haversineKm } from '../common/geo.util';
import { GraphAdjacency, RoadEdge, RoadNode } from './road-graph.service';

export interface RouteCostWeights {
Expand Down Expand Up @@ -216,7 +217,7 @@ export class PathfindingService {
* A* heuristic: straight-line distance converted to approximate cost (Prompt 6).
*/
private heuristic(from: RoadNode, to: RoadNode, weights: RouteCostWeights): number {
const distKm = this.haversineKm(from.lat, from.lng, to.lat, to.lng);
const distKm = haversineKm(from.lat, from.lng, to.lat, to.lng);
const estimatedTimeS = (distKm / 50) * 3600; // assume 50 km/h
return weights.timeWeight * estimatedTimeS;
}
Expand Down Expand Up @@ -386,17 +387,4 @@ export class PathfindingService {
private pathKey(path: PathResult): string {
return path.nodes.map(n => n.id).join(',');
}

private haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLng / 2) *
Math.sin(dLng / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
}
20 changes: 6 additions & 14 deletions engine-backend/src/modules/routing/routing.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from '@nestjs/common';
import * as h3 from 'h3-js';
import { DatabaseService } from '../../database/database.service';
import { haversineKm } from '../common/geo.util';
import { RoadGraphService } from './road-graph.service';
import { PathfindingService } from './pathfinding.service';
import { TrafficService } from './traffic.service';
Expand Down Expand Up @@ -118,7 +119,9 @@ export class RoutingController {
hit_count = route_cache.hit_count + 1,
expires_at = EXCLUDED.expires_at`,
[originH3, destH3, hour, JSON.stringify(result)],
).catch(() => { /* non-critical */ });
).catch((err: unknown) => {
this.logger.warn(`[ROUTE_CACHE] DB write failed (non-critical): ${err instanceof Error ? err.message : String(err)}`);
});

return result;
}
Expand Down Expand Up @@ -197,7 +200,7 @@ export class RoutingController {

if (!originNode || !destNode) {
// Fallback: simple haversine estimate
const distKm = this.haversineKm(olat, olng, dlat, dlng);
const distKm = haversineKm(olat, olng, dlat, dlng);
const travelTimeS = (distKm / 40) * 3600;
return {
travelTimeS: Math.round(travelTimeS),
Expand Down Expand Up @@ -230,7 +233,7 @@ export class RoutingController {

// Fallback route when graph is empty or disconnected
private fallbackRoute(dto: RouteRequestDto) {
const distKm = this.haversineKm(
const distKm = haversineKm(
dto.origin.lat, dto.origin.lng,
dto.destination.lat, dto.destination.lng,
);
Expand All @@ -257,15 +260,4 @@ export class RoutingController {
fallback: true,
};
}

private haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLng = ((lng2 - lng1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos((lat1 * Math.PI) / 180) * Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLng / 2) * Math.sin(dLng / 2);
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
}
18 changes: 3 additions & 15 deletions udie_backend_py/app/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import asyncio
import time
from dataclasses import dataclass
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from math import exp

import httpx
Expand All @@ -19,6 +19,7 @@
RoutePoint,
SourceStatus,
TrafficForecastResponse,
_haversine_km,
now_utc,
)
from app.sources.base import GovernmentSource, SourceResult
Expand Down Expand Up @@ -204,8 +205,6 @@ def traffic_forecast_for_point(lat: float, lng: float, horizon_minutes: int = 15
Short-term traffic forecast using exponential smoothing over stored risk cells (Prompt 18).
Uses the in-memory risk store as a proxy for congestion intensity.
"""
from math import exp

risk_cells = store.get_area_risk_cells(lat=lat, lng=lng, radius_km=1.0)
avg_risk = sum(r for _, r in risk_cells) / max(1, len(risk_cells)) if risk_cells else 0.0

Expand Down Expand Up @@ -249,16 +248,7 @@ def compute_navigation_route(
Compute a simple navigation route with risk scoring (Prompt 30).
Uses straight-line polyline with risk-aware ETA.
"""
from math import atan2, cos, radians, sin, sqrt

def haversine_km(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
r = 6371.0
d_lat = radians(lat2 - lat1)
d_lng = radians(lng2 - lng1)
a = sin(d_lat / 2.0) ** 2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(d_lng / 2.0) ** 2
return r * 2.0 * atan2(sqrt(a), sqrt(1.0 - a))

dist_km = haversine_km(origin.lat, origin.lng, destination.lat, destination.lng)
dist_km = _haversine_km(origin.lat, origin.lng, destination.lat, destination.lng)

# Speed by mode
speed_map = {"fastest": 60.0, "shortest": 40.0, "safest": 35.0, "balanced": 45.0}
Expand All @@ -271,8 +261,6 @@ def haversine_km(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
risk_penalty = 1.0 + risk_score * 0.5
travel_time_s = (dist_km / speed_kmh) * 3600.0 * risk_penalty

from datetime import timedelta

arrival = now_utc() + timedelta(seconds=travel_time_s)
polyline = [[origin.lng, origin.lat], [destination.lng, destination.lat]]

Expand Down
11 changes: 1 addition & 10 deletions udie_backend_py/app/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pathlib import Path

from app.config import settings
from app.models import AreaNewsItem, BoundingBox, DisruptionEvent, RoutePoint, now_utc
from app.models import AreaNewsItem, BoundingBox, DisruptionEvent, RoutePoint, _haversine_km, now_utc


def _resolve_db_path(raw: str) -> Path:
Expand Down Expand Up @@ -528,13 +528,4 @@ def _route_to_cells(points: list[RoutePoint]) -> list[RouteCell]:
return cells


def _haversine_km(lat1: float, lng1: float, lat2: float, lng2: float) -> float:
r = 6371.0
d_lat = math.radians(lat2 - lat1)
d_lng = math.radians(lng2 - lng1)
a = math.sin(d_lat / 2.0) ** 2 + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(d_lng / 2.0) ** 2
c = 2.0 * math.atan2(math.sqrt(a), math.sqrt(1.0 - a))
return r * c


store = UdieStore()
Loading
Loading