One route, one day, one vehicle. The map that tells the day’s story.
Pattern detection (Lesson 10) was the macro view — a year of pings condensed to per-parcel signatures. RouteView is the macro’s opposite number: zoom all the way in. Pick a single (route, day, vehicle). Render a self-contained HTML with an interactive MapLibre map — the truck’s trail as colored GPS dots, every parcel colored served / missed / unknown, the depot, the landfills, a side panel with the load-by-load tonnage breakdown. Drop the file on a USB stick, email it, host it static. The day’s story, one HTML per (route, vehicle).
· Objective
Five modules, one bundled HTML template, three runner modes. All built on top of the engine and segments — zero new spatial work.
routeview/rank.py— for one (route, day), which vehicles touched it, sorted by ping count.routeview/trail.py— GPS pings as colored dots (not a polyline); phase color comes from the L9 segments timeline.routeview/parcel_eval.py— per parcel: served today? Expected by the L10 patterns analysis? Surfaces both in the popup.routeview/render.py— wraps the bundledking.htmltemplate from the v1 RouteView notebook (preserved byte-for-byte) and substitutes the data blobs.routeview/runner.py— file-in / HTML-out driver with three modes: one (route, day, vehicle); all routes on a day; top-N vehicles.
· Build it, step by step
1 Rank vehicles per route-day — routeview/rank.py
For a (route, day) tuple, return the vehicles that meaningfully touched it, sorted by ping count. The runner uses this to pick which vehicle to render (top-1 by default, top-N for the multi-HTML mode). Filters out drive-bys via a min_pings floor — a vehicle with 2 pings on a route polygon was almost certainly just passing through.
ranked = rank_vehicles_for_route_day(
enriched_glob="enriched/2026-01-18/*.parquet",
route_id="R1", day="2026-01-18", min_pings=5,
)
# vehicle_id n_pings first_dt_local last_dt_local
# 815001 843 2026-01-18 06:42:00 2026-01-18 16:18:00
# 815014 18 2026-01-18 09:30:00 2026-01-18 09:35:00
2 Trail as colored dots — routeview/trail.py
Filter enriched pings to (route, day, vehicle). Optionally downsample to keep HTML size sane (default: one ping per 30 seconds → about 1200 dots per shift). Time-range-join against the L9 segments timeline so each ping carries the segment_type it falls into. Map segment_type to a hex color. Emit GeoJSON.
PHASE_COLORS = {
"depot_departure": "#22c55e", # green
"windshield": "#9ca3af", # gray
"collection": "#2563eb", # blue
"dump": "#dc2626", # red
"depot_arrival": "#16a34a",
}
One subtle bug worth knowing about: pyarrow often hands you datetime64[us] not [ns], so the bucket-math for downsampling must coerce to ns first. Caught by the tests on the first run.
3 Parcel evaluation + patterns integration — routeview/parcel_eval.py
For each parcel on the route, three pieces of information per popup:
- Served today? Did any enriched ping for this (day, vehicle) carry this APN? Boolean.
- Expected from patterns (optional, from L10): the weekly1 / weekly2 / biweekly vehicles that the long-period analysis surfaced. If they exist and the parcel wasn’t served, the parcel is colored red (missed); if it was served, green; if no expectation either way, gray.
- Actual vehicle: whichever vehicle this view is showing, with served/not-served context.
parcels_json = build_parcels_geojson(
parcels_wkb_path="parcels_wkb.parquet",
enriched_glob="enriched/2026-01-18/*.parquet",
route_id="R1", day="2026-01-18", vehicle_id="815001",
patterns_chunk_path="patterns/2025-03-01_to_2026-02-28/by_route/R1.parquet",
)
This is the validation loop: the macro prediction (patterns) overlaid on the micro reality (today’s pings). Generously matching = ops are running to plan. Diverging = a story to investigate.
4 Render the HTML — routeview/render.py
The KING template from the v1 RouteView notebook is bundled as package data at opentrash/routeview/templates/king.html. It’s preserved byte-for-byte except for one transformation: Python f-string expressions were swapped for plain str.format() placeholders so we can render without eval. Literal JS braces (MapLibre tile-URL {x}/{y}/{z}, regex escapes) are doubled in the template so they survive the format pass.
html = build_routeview_html(
route_id="R1", vehicle="815001",
day=date(2026, 1, 18),
day_time_label="06:42 → 16:18",
route_json=route_geojson_string,
pts_json=trail_geojson_string,
parcels_json=parcels_geojson_string,
lf_json=landfills_geojson, sites_json=sites_geojson,
summary_json=summary_blob, stats_json_enriched=stats_blob,
center_lon=-117.20, center_lat=32.70,
rad_note="RAD: ...", arts_note="ARTS: ...",
)
out_path.write_text(html, encoding="utf-8")
No Jinja2 dependency. The template is f-string-shaped already, and str.format() handles the substitution. One less package to install.
5 Three runner modes — routeview/runner.py
The unit of work is one (route, day, vehicle) HTML. Three drivers iterate that unit differently:
render_routeview(route_id, day, vehicle_id, ...)— the singular case. One HTML out.render_all_routes_for_day(day, ...)— pick the top-1 vehicle for each route touched that day. One HTML per route.render_top_n_vehicles(day, top_n=5, ...)— the day’s busiest 5 vehicles, each with HTMLs for every route they meaningfully touched.
The runner also wires in the tonnage join: for each dump segment in the day’s timeline, look up L5 tonnage by vehicle + time-window matching (45-min pad, matching the notebook’s approach). The side panel surfaces per-load tons.
routeview/
2026-01-18/
R1__815001.html one (route, day, vehicle) HTML
R1__815014.html
R2__815002.html
...
6 Tests + commit
11 tests covering rank (sorting + min_pings filter), trail (phase coloring + downsample correctness across datetime precisions), parcel_eval (served / missed / unknown statuses, patterns integration), render (template substitution + no unfilled placeholders + tile-URL literals preserved), and an end-to-end runner test that produces a real HTML on disk.
pip install -e ".[dev,geotab,postgres]"
ruff check .
pytest -q
git add . && git commit -m "Lesson 11: RouteView — interactive map product"
git push origin main
· Package anatomy after this lesson
Where everything lives now. new marks files added this lesson.
· What you built
- The micro counterpart to pattern detection — one (route, day, vehicle) rendered as a self-contained interactive HTML.
- GPS pings as colored dots — granularity preserved, phase legible at a glance, irregularities visible (which a polyline would smooth over).
- Parcel popups with patterns integration — the macro view’s expectations overlaid on the micro view’s reality. The validation loop made visual.
- Tonnage on the load-by-load panel — L5 tonnage joined in at render time via vehicle + time-window matching (no spatial work).
- Three runner modes — one HTML, all routes on a day, top-N vehicles. The atomic unit (one HTML per route×vehicle) is shared; the drivers differ only in iteration.
- Zero spatial joins. Every input was already produced by the engine, segments, parcels_wkb, or static layers. RouteView is pure rendering.
· Companion resources
Optional, for going deeper.
- MapLibre GL JS: official docs — the open-source fork of Mapbox GL JS, fully community-maintained. Sources, layers, sprites, expressions: everything that makes the KING template tick.
- Tile servers: the template uses OpenStreetMap raster tiles by default. For production map quality, swap in a vector-tile provider (Stadia, MapTiler, Protomaps) by editing the
sourcesblock in the template. - Self-contained HTML as a product: single-file deliverables travel well — USB sticks, email attachments, static hosting. The same pattern as Bokeh embeds or Plotly's interactive export. Reliable, low-friction, no servers to maintain.
· Next lesson
Lesson 12 — Publishing: tag a release, push to PyPI, finalize the documentation site, and write the README. The last lesson takes the working package and turns it into a real open-source artifact that other agencies can pip-install and run.