Indoor accessibility, drawn from data.
A complete technical reference for how AccessMap works: the floor JSON, the per-profile routing, and the tool-using assistant. Readable in an afternoon.
Floors are JSON, not artwork
Every floor is a JSON document with rooms, doors, and a routing graph. The renderer walks that document to draw SVG; the pathfinder walks the same graph to plan routes. Adding a new floor means writing a new JSON file, not editing artwork.
A floor lists rooms as 2-D polygons in a flat coordinate frame, doors as points on shared walls, and a graph of nodes and edges that lives alongside the geometry. Edges carry feature tags like stairs, elevator, ramp, or narrow_passage so we can reweight them per-profile.
{
"outline": [{ "x": 0, "y": 0 }, { "x": 100, "y": 0 }, ...], // exterior boundary
"walls": [{ "id": "win-101", "kind": "window",
"start": { "x": 8, "y": 50 }, "end": { "x": 22, "y": 50 } }],
"rooms": [{ "id": "room-101", "code": "101", "polygon": [...] }],
"doors": [{ "id": "d-101", "between": ["room-101", "corridor"], "position": {...} }],
"nodes": [{ "id": "n-elev", "roomId": "elevator-shaft", "features": ["elevator"],
"connectsToFloor": { "floorSlug": "first", "nodeId": "n-elev" } }],
"edges": [{ "id": "e-c4-stair", "from": "c4", "to": "n-stair", "features": ["stairs"] }]
}Schema source: src/lib/map/schema.ts, a Zod schema that validates the JSON and exports the matching TypeScript types.
It looks like a real floor plan
The renderer composes the SVG in the same order an architect would draw the plan: floor base, room fills, interior partitions, exterior shell, windows, doors, then labels and route on top.
- Outline, an optional polygon for the building's exterior boundary, drawn as a filled “floor base” with a thick stroke around its edges.
- Walls, line segments tagged
exterior,interior, orwindow. Windows render as a cyan glass cut over the wall ink. - Routes, drawn twice: once thick in the page background colour as a halo, once in the brand colour on top. That's the same trick Google Maps and Apple Maps use, and it keeps the line readable on top of any room fill.
A* over a weighted graph
Once a floor is a graph, finding a route is a textbook A* search. The heuristic is plain Euclidean distance, the cost of an edge is its base length times a profile multiplier, and Infinity blocks an edge entirely.
- Heuristic: straight-line distance to the goal. Admissible because every edge cost is at least its straight-line length.
- Cost function:
baseCost × max(profile.multiplier[feature])over the edge's feature tags. - Priority queue: a linear scan over the open set. Fine for teaching graphs and floor-plan-sized inputs; would be the first thing to swap in for a real deployment.
Source: src/lib/map/pathfind.ts, about 100 lines, with pathfind.test.ts next to it.
One graph, many floors
A node with `connectsToFloor` becomes a cross-floor edge. The pathfinder builds a single unified graph keyed by `floor:node` and runs Dijkstra over it: same code path for a one-floor walk and a five-floor traversal.
- Cross-floor edges inherit the source node's features. An
n-elev↔n-elevlink carrieselevator, so the wheelchair profile applies the same slight surcharge it would on an in-floor elevator edge, and a stairs link is hard-blocked the same way. - The result is a list of per-floor segments. The UI shows the current floor's segment with the route halo and offers a shortcut to switch to the next floor where the route continues.
- Heuristic is
0(Dijkstra) since A*'s euclidean heuristic doesn't generalise across floors. Floor-plan-sized graphs are tiny, so this stays trivially fast.
Source: src/lib/map/multi-pathfind.ts, with multi-pathfind.test.ts covering the cross-floor cases.
Profiles change the cost, not the graph
The same map graph yields different routes for different users. Each profile is a map from feature tag to weight multiplier. Set a feature to ∞ and any edge with that tag drops out of the search.
| Feature | Default | Wheelchair | Visually impaired |
|---|---|---|---|
stairs | 1× | ∞ (blocked) | 1.5× |
step | 1× | ∞ (blocked) | 1× |
narrow_passage | 1× | 3× | 1.5× |
ramp | 1× | 1.1× | 1× |
elevator | 1× | 1.2× | 1× |
Two tools and a cached prompt
The assistant exposes exactly two tools: find_room and find_route. Both are thin wrappers around the search and routing code already shipping in the app. There is no parallel implementation.
- Map JSON in the system prompt, marked with
cache_control: ephemeral. Turn 1 writes the cache; subsequent turns read it back at roughly a tenth of the price. - Tool runner loop handled by the official SDK. The agent loop, tool execution, and tool-result plumbing are all in
client.beta.messages.toolRunner(). - Captured route: when
find_routesucceeds, its node list is pushed back into the samehighlightedRouteprop the manual route picker uses, so the visualisation is identical.
Source: src/lib/ai/assistant.ts, route handler at src/app/api/assistant/route.ts.
What's running
Every piece of the stack, and why it's there.
Next.js 16 (App Router)
Routing, layouts, server components.
TypeScript + Zod
Schema and types from one source.
Tailwind v4 + shadcn/ui
Design tokens compose into utility classes; primitives via Base UI.
Inter + Geist Mono
Body in Inter, code in Geist Mono. Both via next/font.
next-themes
Light, dark, and system theme, persisted to localStorage.
Leaflet (CRS.Simple)
Pan, pinch-zoom, mobile gestures over a flat coordinate frame.
Prisma + SQLite
Catalog of buildings and floors. Geometry stays in JSON.
Anthropic SDK
Adaptive thinking, betaZodTool tool runner, prompt caching.
Vitest
Pathfinder unit tests live next to the implementation.
Tokens and type scale
Color tokens
Brand
--brand
Indigo-violet, primary CTAs, brand mark.
Brand strong
--brand-strong
Hover/active brand surfaces.
Brand soft
--brand-soft
Tinted brand surfaces, badges.
Route
--route
Highlighted path stroke (with white halo).
Feature
--feature
Cyan accessibility-feature icons.
Foreground
--foreground
Body text, headlines.
Muted fg
--muted-foreground
Captions, secondary copy.
Surface 1
--surface-1
Page background.
Surface 2
--surface-2
Sidebar panels, summary cards.
Floor base
--floor-base
Building interior fill on the map.
Wall
--wall-exterior
Exterior wall ink.
Window
--window-glass
Glass cuts in exterior walls.
Type scale
| Class | Size / line-height · weight | Use |
|---|---|---|
.text-display | 48 / 56 · 700 | Hero headline |
.text-h1 | 32 / 40 · 600 | Page title |
.text-h2 | 24 / 32 · 600 | Section title |
.text-h3 | 18 / 26 · 600 | Subsection |
.text-lead | 17 / 26 · 400 | Intro paragraph |
.text-body | 15 / 24 · 400 | Body copy |
.text-caption | 13 / 19 · 400 | Helper / metadata |
.text-overline | 11 / 14 · 600 | Section kicker |