AccessMap
About AccessMap

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.

Schema

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.

Drawing

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, or window. 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.
Routing

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.

Multi-floor

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-elevn-elev link carries elevator, 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

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.

FeatureDefaultWheelchairVisually impaired
stairs∞ (blocked)1.5×
step∞ (blocked)
narrow_passage1.5×
ramp1.1×
elevator1.2×
Assistant

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_route succeeds, its node list is pushed back into the same highlightedRoute prop 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.

Stack

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.

Design system

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

ClassSize / line-height · weightUse
.text-display48 / 56 · 700Hero headline
.text-h132 / 40 · 600Page title
.text-h224 / 32 · 600Section title
.text-h318 / 26 · 600Subsection
.text-lead17 / 26 · 400Intro paragraph
.text-body15 / 24 · 400Body copy
.text-caption13 / 19 · 400Helper / metadata
.text-overline11 / 14 · 600Section kicker

AccessMap is built as undergraduate teaching material. It is a sibling of accessguide, the campus accessibility map for the University of Macedonia: sharing the goal but not the codebase.