Compare commits

..

5 commits

Author SHA1 Message Date
david kiania
a6d5a19273 Merge: document map-overlay layers + recent features 2026-06-08 22:01:54 +03:00
david kiania
4eef23677b docs: document map-overlay layers + recent map features
README: add a "Map overlay layers" section (how the toggleable layer system
works + how to add a layer in ~2 min), document fleet segmentation / department
colours / legend / POIs, refresh the file tree (layers/), and correct the deploy
note (Coolify auto-deploys on push to main).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 22:01:52 +03:00
david kiania
58525ec73d Merge: overlay hover label (single popup) 2026-06-08 21:53:09 +03:00
david kiania
18501f00d3 feat(map): show overlay station label on hover, single popup
Switch the Shell layer from click→popup to hover: mousemove shows one reused
popup that follows whichever station is under the cursor, mouseleave clears it
— so only ever one label is visible. No click needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-08 21:53:07 +03:00
david kiania
1532ef6ae0 Merge: toggleable map layers + Shell stations overlay 2026-06-08 21:46:32 +03:00
2 changed files with 65 additions and 14 deletions

View file

@ -3,13 +3,17 @@
A single-file map console that **merges live vehicle positions and historical A single-file map console that **merges live vehicle positions and historical
trips** into one view for the Fireside Communications / Tracksolid fleet. trips** into one view for the Fireside Communications / Tracksolid fleet.
> **Status:** v2 — **feature-frozen 2026-06-07** (two-tier bottom dock + clustering > **Status:** v2 — live at <https://fleetnow.rahamafresh.com>. Deployed from this
> + tracker/camera dedup). Live at <https://fleetnow.rahamafresh.com>. Deployed > repo via Coolify (Dockerfile → nginx). Reads the `dashboard_api` read-API at
> from this repo via Coolify (Dockerfile → nginx). Reads the `dashboard_api` > `fleetapi.rahamafresh.com`.
> read-API at `fleetapi.rahamafresh.com`.
> >
> Deploy is **manual** — push, then hit Redeploy in Coolify (no auto-deploy > **2026-06-08 additions:** fleet segmentation (specialist vehicle icons that
> webhook wired yet). > never cluster), per-department colour coordination + collapsible **Key** legend,
> persistent POIs (Fireside HQ, Safaricom HQ), and a **toggleable map-overlay
> layer system** (first layer: 232 Shell fuel stations). See *Map overlay layers*.
>
> **Deploy auto-fires on push to `main`** via Coolify (~23 min build); if it
> lags, hit Redeploy in Coolify.
## What it does ## What it does
@ -30,6 +34,15 @@ trips** into one view for the Fireside Communications / Tracksolid fleet.
Leaflet.MarkerCluster style, via `supercluster`); click a bubble to zoom and Leaflet.MarkerCluster style, via `supercluster`); click a bubble to zoom and
expand it. Clusters disband into individual pins at ~city zoom (z11). Clustering expand it. Clusters disband into individual pins at ~city zoom (z11). Clustering
honours the active filter and applies to the live view only (not trip routes). honours the active filter and applies to the live view only (not trip routes).
- **Fleet segmentation & department colours.** Specialist vehicles (crane /
motorbike / pick-up) get their own marker icons and are **never clustered**
(always individually visible); every cost centre has a fixed, distinct colour,
with a collapsible **Key** legend (bottom-left) listing only the centres on
screen. Non-operational vehicles (personal / management / Uganda-MTN) are
filtered out upstream in the read-API, so the live map shows the operational
fleet only.
- **Persistent POIs** — Fireside HQ and Safaricom HQ (Waiyaki Way) as labelled
reference markers.
- **Filters** (bottom-right card) apply to the live map *instantly*: - **Filters** (bottom-right card) apply to the live map *instantly*:
- **Number plate** — multi-select, sorted A→Z; picking a vehicle auto-fills its - **Number plate** — multi-select, sorted A→Z; picking a vehicle auto-fills its
cost centre + city. cost centre + city.
@ -64,9 +77,10 @@ GL JS loaded from a CDN). It has no build step and no local assets. It reads fro
the existing dashboard read-API — it does **not** talk to the database directly. the existing dashboard read-API — it does **not** talk to the database directly.
``` ```
index.html → the entire SPA index.html → the entire SPA
Dockerfile → bakes index.html into an nginx:alpine image (port 80) layers/*.geojson → static overlay data (gas stations, …), served at /layers/
nginx.conf → static serve + /healthz + no-cache on index.html Dockerfile → bakes index.html + layers/ into an nginx:alpine image (port 80)
nginx.conf → static serve + /healthz + no-cache on index.html
``` ```
### Backend it depends on ### Backend it depends on
@ -86,6 +100,38 @@ exposes:
(`DASHBOARD_CORS_ORIGINS`). It is in the code default; make sure the deployed (`DASHBOARD_CORS_ORIGINS`). It is in the code default; make sure the deployed
`dashboard_api` container's env includes it, then restart that container. `dashboard_api` container's env includes it, then restart that container.
## Map overlay layers
Toggleable reference overlays (gas stations, etc.) sit behind the **Layers**
control (top-right, collapsed, all **off** by default). Each is a static GeoJSON
in `layers/`, rendered as a MapLibre **symbol** layer that **auto-declutters**
(`icon-allow-overlap: false` — sparse when zoomed out, all points reveal as you
zoom in), with a zoom-scaled icon (~8→16 px) and a **hover** label (one reused
popup, so only ever one is visible). Overlays render *under* the vehicle markers.
Shipped layers:
| Layer | Data | Points |
|---|---|---|
| Shell stations | `layers/shell_stations.geojson` (OSM `kenya-260605`) | 232 |
**To add a layer (≈2 min):**
1. Drop its point GeoJSON in `layers/<name>.geojson`.
2. Add one entry to the `OVERLAYS` array near the top of the `<script>` in
`index.html`:
```js
{ id: '<name>', label: '<Label>', url: 'layers/<name>.geojson',
iconSvg: <40×40 SVG string>, nameKey: 'name', defaultOn: false }
```
`iconSvg` is registered as the marker image (reuse `SHELL_ICON_SVG` as a
template). Nothing else to wire — `addOverlay()` builds the source + symbol
layer, and the Layers control lists it automatically.
3. Commit + push. The `Dockerfile` already `COPY`s `layers/` into nginx.
> The Shell layer was extracted from a Kenya OSM `.pbf` — the reproducible
> workflow (filter `amenity=fuel`, `brand=Shell`) lives in the `tracksolid` repo:
> `scripts/export_osm_pois.py` + `docs/OSM_POI_EXPORT.md`.
## Deploy (Coolify, git-based) ## Deploy (Coolify, git-based)
1. In Coolify, create a new **Application** from this git repo 1. In Coolify, create a new **Application** from this git repo

View file

@ -635,6 +635,7 @@ const OVERLAYS = [
iconSvg: SHELL_ICON_SVG, nameKey: 'name', defaultOn: false }, iconSvg: SHELL_ICON_SVG, nameKey: 'name', defaultOn: false },
// future layers: add { id, label, url, iconSvg, nameKey, defaultOn } here. // future layers: add { id, label, url, iconSvg, nameKey, defaultOn } here.
]; ];
let overlayPopup = null; // single reused hover popup for overlay points (only one ever shown)
function registerOverlayIcon(def) { function registerOverlayIcon(def) {
return new Promise(resolve => { return new Promise(resolve => {
@ -667,14 +668,18 @@ async function addOverlay(def) {
'visibility': def.defaultOn ? 'visible' : 'none', 'visibility': def.defaultOn ? 'visible' : 'none',
}, },
}); });
map.on('click', lyrId, e => { // Hover (not click) shows a single label — one reused popup, so only ever
const p = (e.features[0] && e.features[0].properties) || {}; // one is visible; mousemove keeps it on whichever station is under the cursor.
popup.setLngLat(e.lngLat) map.on('mouseenter', lyrId, () => { map.getCanvas().style.cursor = 'pointer'; });
map.on('mousemove', lyrId, e => {
const f = e.features[0]; if (!f) return;
const p = f.properties || {};
if (!overlayPopup) overlayPopup = new maplibregl.Popup({ closeButton: false, closeOnClick: false, offset: 12 });
overlayPopup.setLngLat(f.geometry.coordinates)
.setHTML(`<div class="ov-pop"><b>${escapeHtml(p[def.nameKey] || def.label)}</b><div class="ov-sub">${escapeHtml(p.brand || 'fuel station')}</div></div>`) .setHTML(`<div class="ov-pop"><b>${escapeHtml(p[def.nameKey] || def.label)}</b><div class="ov-sub">${escapeHtml(p.brand || 'fuel station')}</div></div>`)
.addTo(map); .addTo(map);
}); });
map.on('mouseenter', lyrId, () => { map.getCanvas().style.cursor = 'pointer'; }); map.on('mouseleave', lyrId, () => { map.getCanvas().style.cursor = ''; if (overlayPopup) overlayPopup.remove(); });
map.on('mouseleave', lyrId, () => { map.getCanvas().style.cursor = ''; });
} }
buildLayersControl(); buildLayersControl();
} }