Initial implementation of the public trips dashboard: - db/migrations/001..005: read-only viz_anon role + thin trips_viz_v1 view + three SECURITY DEFINER RPCs (trips_for_day, trips_for_range, list_cost_centres). Builds path on demand from position_history; coalesces missing cost_centre to 'Unassigned'. Smoke-tested against staging: 982 trips / 13 cost centres for 2026-04-29. - compose/: PostgREST v12 service + trips_web Caddy service. CORS allow-listed to the web FQDN; viz_anon role is the only authorization. - web/: Vite + React + TS SPA. deck.gl TripsLayer animated over PathLayer (whole route in low opacity), Mapbox GL dark base map, Zustand store, TanStack Query for fetching. Sidebar = date controls + cost-centre multi-select + vehicle drilldown. Timebar = scrubber with 1x/10x/60x/600x speeds. tsc + vite build clean. - README + design doc updated to match the verified schema (path lives in tracksolid.position_history, vehicle key is imei, no down-sampling needed at observed volume).
62 lines
2.1 KiB
PL/PgSQL
62 lines
2.1 KiB
PL/PgSQL
-- 003_trips_for_day_rpc.sql
|
|
-- Returns one row per trip on p_date, with the path reconstructed from
|
|
-- tracksolid.position_history points falling inside the trip window.
|
|
--
|
|
-- path_geojson : LineString GeoJSON, deck.gl reads {type, coordinates} directly.
|
|
-- timestamps_rel: integer seconds elapsed since trip start, one per vertex.
|
|
-- Frontend offsets these to "seconds since 00:00 of selected date"
|
|
-- so a single TripsLayer can animate every trip on one timeline.
|
|
--
|
|
-- HAVING count >= 2 drops trips with too few points to draw a line (rare,
|
|
-- but happens for very short trips that started/ended between GPS pings).
|
|
|
|
CREATE OR REPLACE FUNCTION public.trips_for_day(
|
|
p_date date,
|
|
p_cost_centres text[] DEFAULT NULL
|
|
) RETURNS TABLE (
|
|
trip_id bigint,
|
|
imei text,
|
|
vehicle_name text,
|
|
vehicle_number text,
|
|
cost_centre text,
|
|
start_time timestamptz,
|
|
end_time timestamptz,
|
|
distance_km numeric,
|
|
path_geojson json,
|
|
timestamps_rel int[]
|
|
)
|
|
LANGUAGE sql STABLE
|
|
SECURITY DEFINER
|
|
SET search_path = public, tracksolid
|
|
AS $$
|
|
WITH day_trips AS (
|
|
SELECT v.*
|
|
FROM public.trips_viz_v1 v
|
|
WHERE v.start_time::date = p_date
|
|
AND (p_cost_centres IS NULL OR v.cost_centre = ANY(p_cost_centres))
|
|
)
|
|
SELECT
|
|
dt.trip_id,
|
|
dt.imei,
|
|
dt.vehicle_name,
|
|
dt.vehicle_number,
|
|
dt.cost_centre,
|
|
dt.start_time,
|
|
dt.end_time,
|
|
dt.distance_km,
|
|
ST_AsGeoJSON(ST_MakeLine(ph.geom ORDER BY ph.gps_time))::json AS path_geojson,
|
|
array_agg(
|
|
EXTRACT(EPOCH FROM ph.gps_time - dt.start_time)::int
|
|
ORDER BY ph.gps_time
|
|
) AS timestamps_rel
|
|
FROM day_trips dt
|
|
JOIN tracksolid.position_history ph
|
|
ON ph.imei = dt.imei
|
|
AND ph.gps_time BETWEEN dt.start_time AND dt.end_time
|
|
AND ph.geom IS NOT NULL
|
|
GROUP BY dt.trip_id, dt.imei, dt.vehicle_name, dt.vehicle_number,
|
|
dt.cost_centre, dt.start_time, dt.end_time, dt.distance_km
|
|
HAVING count(ph.geom) >= 2;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.trips_for_day(date, text[]) TO viz_anon;
|