A map driver: a separate process that renders a Smart Checkpoints project on a real basemap and serves that page to the operator console.
It is deliberately small. It answers no questions, resolves no distances, and
holds no state. Its whole job is to hold a socket open — which is what makes
the map view exist — and to serve one page that speaks the console's
postMessage bridge.
camera ─┐
├─► server ──► console ──postMessage──► this page (iframe)
distance ─┘ ▲ │
driver └────────────┘
Socket.IO
The console holds the single connection to the server and forwards project state into the frame. This driver never opens a socket to the server for data, never talks to the distance driver, and never sees an API key in the browser.
The server must never import a mapping library, and must never know whether the map is Mapbox, Leaflet or something else. Adding an eleventh distance driver has to cost a map driver nothing, and the other way round.
That also means this page cannot be served out of the Smart Checkpoints
server's public/ directory: that directory is wiped and regenerated by every
console build. Hosting its own page from its own process is the point.
npm install
cp .env.example .env # then fill in SC_API_KEY and MAPBOX_TOKEN
npm start| Variable | Meaning |
|---|---|
SC_WS_URL |
The driver channel. Default ws://localhost:3000/distance-driver |
SC_API_KEY |
The project's operator key. Required |
SC_MAP_HOST / SC_MAP_PORT |
Where the page is served. Default 127.0.0.1:4100 |
SC_MAP_PUBLIC_URL |
The address announced to the server, when it differs from the bind address |
MAPBOX_TOKEN |
Mapbox access token. Required, and never committed |
MAPBOX_STYLE |
Any Mapbox style URL. Default light-v11, which is what the console is |
The token is a browser credential — it has to reach the browser to be of any
use — so it is read from the environment and served to the page at /env.js.
It is handed only to whoever can already reach this port, which is why the
server binds to loopback unless told otherwise. Scope it and rotate it at
Mapbox.
Mapbox GL JS is served by this driver, from /vendor/, not from a CDN.
A page that is going to be embedded in a console on another origin cannot
depend on a third-party origin being fetchable from inside an iframe: privacy
extensions and frame-level third-party blocking stop exactly that, and it fails
silently - a blank map view with nothing on screen to say why. npm install
puts the library in place; the page says so plainly if it is missing.
Internet access is still required for tiles: they come from Mapbox. A project on a private camera network with no route out needs a different map driver, and that is exactly why the map is a driver rather than part of the console.
On connecting, this driver tells the server where its page is (uiUrl in the
auth message). That is a proposal, not a setting. Whoever holds a project
API key would otherwise be choosing what renders inside the console's own
chrome: a cross-origin frame cannot read the console, but it is handed every
checkpoint, distance and violation in the project and could paint a convincing
fake console around them.
So the announced address lands in pending_map_driver_url with status
pending, and the console shows it under Project with the origin spelled
out. An operator approves it once. Only then does the console embed it.
- Reconnecting on the same address stays approved — restarting this driver does not send anybody back to the approval screen.
- Announcing a different address goes back to
pending, and the previous approval is dropped: it described a page that is no longer being served.
Changing SC_MAP_PORT or SC_MAP_PUBLIC_URL therefore needs one approval in
the console afterwards. That is the intended cost.
Every message is { v: 2, type, payload }. Both sides pass the other's origin
as targetOrigin and check event.origin on receipt — never "*", in either
direction. The console tells this page its origin by setting
referrerpolicy="origin" on the frame, so document.referrer is the console's
origin and nothing more. Opened outside a console, this page renders nothing:
there is nobody to talk to, and guessing would mean posting a project's
checkpoints at an origin nobody approved.
Console → page
| Type | Payload |
|---|---|
sc:init |
{ projectId, protocolVersion, origin: {lat,lng} } |
sc:graph |
the whole snapshot: nodes with flags, edges with distance, status, limit and path |
sc:node-updated |
one node |
sc:edge-updated |
one edge, including new path |
sc:congestion |
{ [connectionId]: c } |
sc:diagnostics |
node and edge flags |
sc:selection |
{ kind: "node" | "edge" | null, id } |
Page → console
| Type | Payload |
|---|---|
sc:ready |
{ protocolVersion, capabilities } |
sc:select |
{ kind, id } |
sc:node-moved |
{ nodeId, latitude, longitude } — only because this driver declared capabilities.nodeDrag |
Everything arriving from the console is validated here before it is believed, and everything arriving from here is validated by the console. Neither side trusts the other; both trust an origin.
- Edges follow the
patha distance driver measured — the real road shape, a GeoJSONLineStringin WGS84, longitude first. An edge with no geometry is drawn as the straight line between its endpoints, which is what the graph view draws for everything and is the honest thing to show: nobody has said where that road goes. - Solid means enforcing. An edge whose
distance_statusis notokis drawn broken, because it decides no violations. - A yellow ring is a checkpoint the data-quality checks have flagged.
That is what makes the graph/map toggle worth having: both views draw the same geometry, so a checkpoint that looks fine on the graph and sits inside a building on the map has told you something.
An empty map has several causes and they all look identical, so the page keeps a line under the legend saying which one it is:
| On screen | What it means |
|---|---|
connected - waiting for the console to send this project |
The basemap is up and the handshake is done; no snapshot has arrived |
the console sent no checkpoints - this project has none yet |
A snapshot arrived and it was empty |
3 checkpoints · 3 edges · 2 drawn on real roads, 1 straight... |
Drawn - and how much of it is measured road rather than a straight line |
basemap error: ... |
A tile or a glyph failed after the map was up. The map stays usable |
Failures before the basemap is up cover the map instead, because there is nothing behind them worth looking at: no Mapbox token, the library missing, or a style Mapbox refused.
The console has one of its own. If this page never completes the handshake it says so and names the address, because a page that did not load cannot say anything at all - and "the driver's HTTP port is not reachable from this browser" is otherwise indistinguishable from "the map is broken".
Dragging a checkpoint here is a proposal. A basemap is where a bad GPS fix becomes obvious, so it is worth supporting — but moving a camera throws away every distance measured to it, and that decision belongs to the console, which asks first and says how far in metres. On drop the checkpoint goes back where it was and stays there until the console says otherwise, because until then it has not moved.
MIT.