Accessibility-first navigation for the 1 in 4 the city forgot.
Most navigation apps optimize for speed β not accessibility. A missing curb ramp can make an entire route impossible.
AccessMap AI builds a personalized, explainable map of the city for people with different accessibility needs. It combines OpenStreetMap, transit feeds, elevation data, live user-reported hazards, and Gemini-powered image analysis to route around the things other maps pretend aren't there.
Built for HackDavis 2026.
- 1 in 4 US adults lives with a disability β 70M+ people navigating environments not designed for them.
- Most routing engines optimize for "shortest path" over "passable path."
- Static accessibility tags (e.g.
wheelchair=yes) don't capture the real environment: slope, surface, lighting, crowd density, missing ramps, broken sidewalks, or live obstructions.
AccessMap AI replaces "shortest" with per-profile, hazard-aware, explainable routing.
| Feature | What it does |
|---|---|
| Profile-aware routing | 5 accessibility profiles with distinct cost functions over slope, surface, stairs, curb ramps, tactile paving, audible crossings, lighting, noise, and crowd density. Profiles can be combined. |
| Live community hazards | Users drop pins for construction, broken ramps, blocked sidewalks, etc. Hazards are stored in Supabase, tagged with the profiles they affect, and used to steer routes around them and lower the route score when a relevant hazard is on the path. |
| AI sidewalk analysis | Upload a photo of a sidewalk/entrance/crosswalk β Gemini returns surface type, slope estimate, hazards, a 0β100 accessibility score, and a wheelchair verdict, with sanity-checked post-processing. |
| Explainable routes | Every route comes back with a per-factor score breakdown (slope, surface, lighting, hazards, etc.) and a plain-English "Why this route" paragraph. |
| Spoken directions | The Directions card has Read aloud / Pause / Stop controls powered by the browser's Web Speech API. Each turn is queued as its own utterance, the currently-spoken step is highlighted in the list, and speech auto-cancels when the route changes. |
| Heatmaps & overlays | Configurable heatmaps for accessibility_score, noise_score, crowd_score, lighting_score, surface_score, kerb_score. Accessibility-feature points (curb ramps, tactile paving, crossings) are queryable per bounds. |
| Authenticated profiles | Supabase auth with a per-user accessibility profile (routing preference + free-form notes). |
AccessMapAI/
βββ frontend/ Next.js 15 app (landing + dashboard + auth)
β βββ app/
β β βββ page.tsx Landing page (route: /)
β β βββ app/page.tsx Map dashboard (route: /app)
β β βββ login/ Email/password sign-in
β β βββ signup/ Sign-up + email confirmation
β β βββ auth/callback/ PKCE callback for email links
β β βββ profile/ Profile view
β β βββ profile/setup/ Onboarding (routing profile + notes)
β βββ components/
β β βββ access-dashboard.tsx Main map UI, profile picker, hazards, AI panel
β β βββ access-map/ Mapbox map + layer types
β β βββ auth-provider.tsx Supabase client + session/profile state
β βββ lib/ api.ts, supabase/, hazard-labels, profile-types
β
βββ backend/ FastAPI + accessibility pipeline
β βββ main.py App entry, lifespan pipeline boot, routes
β βββ api/
β β βββ vision.py POST /analyze-sidewalk (Gemini)
β β βββ hazards.py GET/POST /hazards (Supabase-backed)
β βββ pipeline/
β β βββ enrichment.py run_pipeline(): builds the enriched graph
β β βββ graph_builder.py OSM ways β NetworkX nodes/edges
β β βββ elevation.py Open-Meteo batch β slope per edge
β β βββ gtfs.py Loads Unitrans GTFS β stops/routes
β β βββ scoring.py Heuristic scores (crowd, noise, lighting, β¦)
β βββ routing/
β β βββ engine.py Dijkstra w/ profile + hazard cost function
β β βββ profiles.py 5 profiles + combined-profile builder
β βββ scripts/ DB migration / RLS scripts
β
βββ data/ OSM extracts + GTFS for UC Davis / Davis, CA
β βββ osm/ sidewalks, buildings, roads, accessibility, lighting
β βββ gtfs/unitrans/ 292 stops Β· 22 routes Β· 7,673 trips
β
βββ supabase/ Auth + tables
β βββ README.md Project setup steps
β βββ migrations/ profiles table + RLS policies + triggers
β
βββ README.md You are here.
You need three things running: Supabase project, backend, frontend.
- Create a project at supabase.com.
- Authentication β URL configuration: site URL
http://localhost:3000, redirect URLhttp://localhost:3000/auth/callback. - SQL Editor β paste
supabase/migrations/20260209120000_profiles.sqlβ Run. - Create a
hazardstable (or runbackend/scripts/setup_tables.sql):create table public.hazards ( id uuid default gen_random_uuid() primary key, lat double precision not null, lon double precision not null, type text not null, description text default '', affected_profiles text[] default '{}', created_at timestamptz default now() );
- Note your
Project URLandanon/service_rolekeys from Project Settings β API.
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txtCreate backend/.env:
GEMINI_API_KEY=your_gemini_key_here # or "mock" to use the canned demo response
SUPABASE_URL=https://<project>.supabase.co
SUPABASE_KEY=<service_role_key> # used server-side for hazard insertsRun:
uvicorn main:app --reload --port 8000The pipeline boots once at startup (~5s) and builds the enriched pedestrian graph (~20k nodes, ~21k edges).
cd frontend
npm installCreate frontend/.env.local:
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_MAPBOX_TOKEN=pk.eyJ...
NEXT_PUBLIC_SUPABASE_URL=https://<project>.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon_key>
# Optional build label shown next to the logo
NEXT_PUBLIC_BUILD_LABEL=devRun:
npm run devOpen:
http://localhost:3000/β landing pagehttp://localhost:3000/appβ interactive map dashboardhttp://localhost:8000/docsβ FastAPI auto docs
| Var | Required | Notes |
|---|---|---|
GEMINI_API_KEY |
for image analysis | Set to mock to return a canned response (handy without burning credits) |
SUPABASE_URL |
yes | For hazard CRUD |
SUPABASE_KEY |
yes | service_role key, server-only |
SKIP_ELEVATION |
optional | true to skip Open-Meteo elevation calls (faster boot, slope=0) |
| Var | Required | Notes |
|---|---|---|
NEXT_PUBLIC_API_URL |
yes | e.g. http://localhost:8000 |
NEXT_PUBLIC_MAPBOX_TOKEN |
yes | Mapbox public token |
NEXT_PUBLIC_SUPABASE_URL |
yes | Same project as backend |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
yes | The anon key (browser-safe) |
NEXT_PUBLIC_BUILD_LABEL |
optional | Shown next to the logo |
NEXT_PUBLIC_GIT_SHA / NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA |
optional | Auto-shortened on the build badge |
Next.js does not load
.env.local.example. Always copy to.env.local.
ββββββββββββββββββββββββ HTTPS ββββββββββββββββββββββββββββββ
β Next.js (App Router)β ββββββββββββββββΊ β FastAPI (uvicorn :8000) β
β /, /app, auth, profile β β
β Mapbox GL Β· shadcn β ββββ JSON βββββ β /route /heatmap /hazards β
ββββββββββββ¬ββββββββββββ β /analyze-sidewalk /stats β
β βββββββββββββββ¬βββββββββββββββ
β Supabase (auth + profiles + β
β hazard table) β
βΌ βΌ
ββββββββββββββββββββ ββββββββββββββββββββββ
β Supabase β β Routing Engine β
β β’ auth.users β β NetworkX + KDTree β
β β’ profiles β β Dijkstra w/ profileβ
β β’ hazards β ββββ 30s cache βββββββΊβ + hazard cost fn β
ββββββββββββββββββββ βββββββββββ¬βββββββββββ
β
βββββββββββΌβββββββββββ
β Data Pipeline β
β OSM β’ GTFS β’ β
β Open-Meteo β’ Heuristics
ββββββββββββββββββββββ
β²
β Gemini API
βββββββββββ΄βββββββββββ
β Vision endpoint β
β /analyze-sidewalk β
ββββββββββββββββββββββ
Defined in backend/routing/profiles.py. Each profile sets weights for the cost function below.
| Profile | Hard constraints | What it strongly prefers |
|---|---|---|
| wheelchair | avoid stairs Β· max_slope=8.33% (ADA) Β· min_width=1.2m Β· avoid unpaved |
smooth surfaces (0.9), gentle slopes (0.95), curb ramps (0.9), explicit sidewalks (0.8) |
| blind | β | tactile paving (0.8), signalized crossings (0.85), low noise (0.8), low crowds (0.7), consistent surfaces (0.7) |
| elderly | max_slope=10% |
gentle slope (0.8), good lighting (0.7), surface quality (0.7), low crowds (0.5) |
| neurodivergent | β | low noise (0.95), low crowds (0.95), some lighting (0.4) |
| temporary_injury | avoid stairs Β· max_slope=12% |
smooth surfaces (0.6), low slope (0.7), strong stairs aversion (stairs_penalty=10) |
| default | β | balanced 0.3β0.4 across factors |
Combining profiles: when multiple are selected, weights take the max (most demanding factor wins) and hard constraints become the most restrictive (e.g. wheelchair + temporary*injury β stairs avoided, slope cap min). Combined name becomes combined*<a>\_<b>.
Dijkstra over a NetworkX graph using a per-profile cost function:
edge_cost = distance_m * (1 + penalty_sum)
avoid_stairs& edge has stairs β Γ1000 (effectively impassable)requires_width& edge width below it β Γ50max_slopeexceeded β Γ20avoid_unpaved& poor surface (gravel/dirt/mud) β Γ15- Hazard within 25 m of the edge that affects the user's profile β Γ50
slope, surface, noise, crowd, lighting, kerb, stairs, crossing_signal_score (bonus), tactile_score (bonus), is_sidewalk (bonus). Each weighted by the profile.
After Dijkstra, every reported hazard whose affected_profiles overlaps the user's selected profiles is checked against every node on the path:
| Distance to nearest path node | Penalty |
|---|---|
| < 15 m | 0.40 (high) |
| < 30 m | 0.25 (medium) |
| < 60 m | 0.10 (low) |
| β₯ 60 m | ignored |
Penalties cap at 0.7. Output:
- New
scores.hazardsbucket =1 - hazard_penalty scores.overallis reduced by0.4 * hazard_penaltyhazards_on_routearray surfaced in the response (id, type, distance, severity, β¦)- Explanation appends a heads-up sentence like "Heads up: 2 reported hazard(s) for your profile lie close to this route (1 within 30m, 1 within 60m) β score adjusted accordingly."
_generate_explanation() aggregates path stats (avg slope, surface, lighting, noise, crowd, crossing-signal, tactile, sidewalk ratio) and emits a profile-aware paragraph in plain English.
The Directions card in /app can read each turn out loud β useful for blind / low-vision users, or anyone who can't keep their eyes on the screen.
- Implemented as a
useDirectionsSpeechhook infrontend/components/access-dashboard.tsxover the browser'swindow.speechSynthesisAPI β no extra deps, no server round-trip. - Each direction step is queued as its own
SpeechSynthesisUtterance, formatted as"Step N. <instruction>. <distance> meters.". onstart/onendcallbacks update acurrentStepstate, which highlights the active step in the list (primary tint + filled badge) so visual users can follow along.- Controls: Read aloud (or Resume when paused) Β· Pause Β· Stop. Buttons only render when
speechSynthesisis supported, so older browsers degrade gracefully. - Speech is automatically cancelled when
routeData.directionschanges (new route) and on component unmount, so old utterances never bleed into a freshly-computed route. - All controls have explicit
aria-labels for screen-reader users.
Note: the Web Speech API uses voices already installed on the user's OS; quality varies by platform.
backend/pipeline/enrichment.py runs once at server startup (lifespan).
graph_builder.pyβ Loads OSM JSON (sidewalks, paths, steps), de-dupes nodes, builds a NetworkX graph with edges taggedsurface,width,incline,kerb,tactile_paving,lit,wheelchair,has_stairs,is_sidewalk.elevation.pyβ Batches edge midpoints to Open-Meteo Elevation (free, no key) β computesslope(%) per edge from rise/run.gtfs.pyβ Loads Unitrans GTFS βstops,routes.scoring.pyβ Computes derived per-edge scores:Score Method surface_scoreOSM surfacelookup table (asphalt=1.0,gravel=0.4,mud=0.1, β¦)kerb_scoreKDTree over kerb=*features (flush=1.0,lowered=0.9,raised=0.3)tactile_scoreKDTree over tactile_paving=yescrossing_signal_scoreKDTree over highway=crossingwithcrossing=traffic_signalslighting_scoreKDTree over street lamps + lit=yes; daytime=1.0, night β lamp densitynoise_scoreFHWA line-source attenuation: distance to nearest road weighted by class crowd_scoreTime-of-day curve Γ building density Γ campus hotspot proximity (Memorial Union / Silo / ARC) accessibility_scoreWeighted combination of the above
| Dataset | File | Records |
|---|---|---|
| Sidewalks & paths (UC Davis) | data/osm/sidewalks_paths.json |
13,832 elements |
| Sidewalks & paths (Davis) | data/osm/davis_all_sidewalks.json |
10,070 elements |
| Buildings | data/osm/buildings.json |
1,109 |
| Roads | data/osm/roads.json |
24,301 elements |
| Accessibility features | data/osm/accessibility_features.json |
1,180 |
| POIs / amenities | data/osm/davis_amenities.json |
1,177 |
| Street lighting | data/osm/davis_lighting.json |
1,104 |
| Unitrans GTFS | data/gtfs/unitrans/ |
292 stops Β· 22 routes Β· 7,673 trips |
Built graph: ~19,700 nodes Β· ~20,700 edges, ~5 s pipeline boot.
backend/api/hazards.py is a thin layer over a Supabase hazards table.
POST /hazardsβ payload{lat, lon, type, description, affected_profiles[]}. Inserts a row, busts the in-memory cache.GET /hazardsβ returns all current hazards (used by the map markers).- 30-second in-process cache so
/routedoesn't hammer Supabase on every call.
Frontend flow: in /app, click "Report hazard" β click on the map β fill out type, description, and affected profiles β submit. The pin appears immediately on the map and starts affecting routes.
backend/api/vision.py exposes POST /analyze-sidewalk (multipart form, single image file).
Prompted output schema (validated by Pydantic):
{
"overall_score": 78, // 0-100, HIGHER = BETTER
"surface_type": "concrete",
"slope_estimate": "gentle (2-4%)",
"hazards": [
{ "type": "Obstruction", "description": "...", "severity": "medium" }
],
"wheelchair_accessible": true,
"explanation": "..."
}Gemini occasionally inverts the score axis (treating 0 as "best"). _postprocess_result re-anchors:
wheelchair_accessible=trueand no high-severity hazards but score < 35 β score is lifted to 65β85.wheelchair_accessible=falseand any high-severity hazard but score > 55 β score is capped at 35.
If GEMINI_API_KEY is unset or mock, the endpoint returns a canned demo response (with a 2.5 s artificial delay) so the UI can be demoed without burning credits.
- Supabase Auth with email/password + PKCE callback at
/auth/callback. - A
profilesrow is auto-created on signup via theon_auth_user_createdtrigger. - Onboarding (
/profile/setup) collects routing profile + free-text notes (mobility_notes,sensory_notes,additional_needs) and flipsonboarding_completed. AuthProvider(frontend/components/auth-provider.tsx) gates the dashboard: ifuser && !profile.onboarding_completed, it forces/profile/setup.- Sign-out drops back to the landing page (
/).
RLS policies ensure each user only sees/edits their own profile row.
| Route | Purpose |
|---|---|
/ |
Marketing landing page |
/app |
Map dashboard (origin/dest pinning, profile picker, hazards, route panel, AI panel) |
/login |
Email/password sign-in (?next= supported, defaults to /app) |
/signup |
Email/password sign-up (sends confirmation email) |
/auth/callback |
PKCE callback (server fallback at app/api/auth/callback) |
/profile |
Profile view + edit |
/profile/setup |
Onboarding flow (forced after first signup) |
All endpoints are FastAPI auto-documented at http://localhost:8000/docs.
| Method | Path | Purpose |
|---|---|---|
| GET | / |
Health/version + pipeline stats |
| GET | /profiles |
List accessibility profiles |
| POST | /route |
Compute a profile-aware route (body: origin_lat, origin_lon, dest_lat, dest_lon, profiles[]) |
| GET | /route |
Same as POST, query-string version |
| GET | /heatmap |
Heatmap for a metric within bounds (metric, north/south/east/west) |
| GET | /transit |
Unitrans stops & routes |
| GET | /edge/{u}/{v} |
Inspect raw edge data |
| GET | /stats |
Pipeline stats (counts + score distributions) |
| GET | /accessibility-points |
Categorized accessibility features in bounds |
| GET / POST | /hazards |
List or create hazards |
| POST | /analyze-sidewalk |
Gemini image analysis (multipart) |
Frontend β Next.js 15 (App Router) Β· React 19 Β· TypeScript Β· Tailwind v4 Β· shadcn/ui Β· react-map-gl (Mapbox GL JS v3) Β· lucide-react Β· @supabase/ssr
Backend β FastAPI Β· Uvicorn Β· NetworkX Β· SciPy KDTree Β· NumPy Β· Pandas Β· Shapely Β· httpx Β· google-genai Β· supabase (python) Β· python-dotenv
Data β OpenStreetMap (Overpass API) Β· Unitrans GTFS Β· Open-Meteo Elevation Β· Mapbox basemap (navigation-night-v1)
Infra β Supabase (Postgres + auth + RLS)
- Geographic scope: dataset is currently Davis, CA + UC Davis. Adding a new city = drop fresh OSM extracts in
data/osm/and re-boot. - Elevation: Open-Meteo is free but rate-limited;
SKIP_ELEVATION=trueis supported for fast iteration. - Heuristics: noise/crowd/lighting are derived (not measured). They're calibrated to Davis hotspots; they will need recalibration for other cities.
- Image analysis: Gemini is a probabilistic model. The post-processor handles the most common score-inversion failure mode but cannot catch subtle errors.
- Hazard table: the schema is intentionally simple. There's no expiry/voting system yet β all reported hazards are treated as currently active.
- Hazard expiry / community confirmation flow
- Multi-city onboarding (city picker + dataset hot-swap)
- Indoor accessibility (entrances, elevators) via OSM
indoor=* - ML-driven sidewalk segmentation from street imagery
- Aggregated heatmap of "missing infrastructure" (places where the routing engine routinely refuses paths)
Built for HackDavis 2026. Open data: OpenStreetMap contributors Β· Unitrans GTFS Β· Open-Meteo. AI: Google Gemini. Map: Mapbox.
{ "origin": { "lat": 38.5382, "lon": -121.7541 }, "destination": { "lat": 38.5421, "lon": -121.7493 }, "profiles": ["wheelchair"], "profile_display": "Wheelchair User", "distance_m": 612.4, "path": [{ "lat": ..., "lon": ..., "node_id": ... }, ...], "explanation": "This route is 612 meters long, avoiding all stairs, ...", "directions": [...], "scores": { "overall": 0.83, "slope": 0.94, "surface": 0.91, "noise": 0.71, "crowd": 0.62, "lighting": 0.55, "kerb": 0.88, "crossing_signals": 0.42, "tactile": 0.30, "hazards": 0.90 }, "geojson": { "type": "Feature", "geometry": { "type": "LineString", ... } }, "hazards_on_route": [ { "id": "...", "type": "broken_ramp", "distance_m": 22.0, "severity": "medium", "affected_profiles": ["wheelchair"] } ] }