Skip to content

plotly/plotly-navigation

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Dash Navigation

A self-hosted replacement for Google Maps that views the street map and computes driving directions, using only free/public OpenStreetMap data and doing all the calculation itself. Scoped to a radius around a center point, both configurable (defaults to a 15-mile radius around Madison, WI).

No Google, no Mapbox account, no external tile servers. The only required network access is a one-time OSM data download; live traffic (optional, see below) is the only other outbound call.

A single-file Dash app (app.py) you run with one command:

uv run app.py      # then open http://localhost:8051 (set PORT to change)

Requires uv. No manual install or venv — uv reads pyproject.toml and builds the environment on the first run.

Click once to drop a start point, click again for a destination; the route and turn-by-turn directions appear in the sidebar. On first run it loads the street graph from drive_network.graphml (next to the script, or under data/) if present, otherwise downloads it from OSM once (~1 min).

Architecture

  • Datanetwork_type="drive" graph from OSM (one-way streets respected), loaded once at startup.
  • Tiles — the map basemap is drawn to raster PNG tiles (Pillow) on the fly and served from a Flask route, displayed with dash-leaflet. No pre-baked tiles or external tile server.
  • Routing — a hand-written A* with a haversine heuristic over travel time, not a library shortest-path call.
  • Directions — derived by walking the path edges, grouping by street name, and classifying each heading change into a turn.

Adjusting the coverage area

Set the LAT, LON, and RADIUS_MILES env vars (or edit their defaults near the top of app.py) and delete the saved drive_network.graphml so it re-downloads for the new area — otherwise the old area's graph gets reused regardless of the new settings. A larger radius means a bigger graph and slower tile generation.

LAT=39.7392 LON=-104.9903 RADIUS_MILES=10 uv run app.py   # e.g. Denver, CO

Known limitations / next steps

  • Tiles show roads only (no water, parks, or buildings).
  • No text labels (street names on the map) — that needs a glyph/font source. Directions already use the real street names.
  • No address search (geocoding). Points are set by clicking. A local geocoder could be built from OSM address data.

Live traffic (optional)

The app can pull real, live traffic data from TomTom's free-tier Traffic Flow Vector Tiles API (no credit card required) and use it two ways:

  • Visual overlay — major roads are colored by current congestion (green → amber → red).
  • Routing — A* and the reported ETA use live speeds where we have a fresh reading, instead of always assuming free-flow conditions.

We looked at government sources first — WisDOT's 511 API only publishes incidents/closures, not live speeds, and WisDOT's real-time sensor feed exists but requires an access request to their traffic operations center rather than self-service signup. TomTom's Vector Flow Tiles turned out to be the best fit even among commercial options: unlike a per-point speed API (which would need one HTTP call per road segment), one vector tile call returns real MVT data — decoded with the same mapbox_vector_tile library our own tiles use — covering hundreds of segments with a traffic_level property (current speed as a fraction of free-flow) per segment. We spatially match those segments to our own OSM graph edges ourselves (TomTom's segment IDs don't correspond to OSM's); TomTom's tile images themselves are never used or displayed.

This is entirely optional and fails soft everywhere: with no key configured, or if TomTom is unreachable or rate-limited, affected edges are just skipped — routing falls back to static OSM speeds and tiles don't color that road — so the rest of the app works exactly as it does today.

To enable it:

  1. Sign up for a free API key at developer.tomtom.com (no cost, no card).

  2. Set it as an environment variable, or drop it in a .env file at the repo root (TOMTOM_API_KEY=your_key_here), before starting the app:

    export TOMTOM_API_KEY=your_key_here
    uv run app.py

When set, a "Live traffic" toggle appears. Traffic tile data is cached ~90s server-side (the browser never talks to TomTom directly, and the key never reaches the client), and a route request only fetches the tiles covering the trip — a handful for a typical route, capped at ~150 nearest the start→end corridor for the longest cross-area trips — so even modest use stays well within the free tier's 200,000-request/month quota.

Deploying

The app runs on Plotly Cloud as-is: plotly app publish (the Plotly Cloud CLI, pip install "dash[cloud]") uploads the files and the platform builds and serves the app. plotly-cloud.toml pins which cloud app gets updated on re-publish, and the one-line Procfile asks the host for threaded workers.

For any other Python host, serve app:server the same way (gunicorn lives in the deploy dependency group):

uv run --group deploy gunicorn app:server --workers 2 --threads 8 --timeout 120

Threaded workers matter: traffic tiles can block on a live TomTom fetch, and a single pan/zoom requests many tiles at once — with single-threaded workers those queue up and tiles load noticeably one by one. Set TOMTOM_API_KEY as an environment variable on the host — don't commit .env.

About

Self-hosted maps in a single Dash file: OSM basemap, A* routing, turn-by-turn directions, live traffic

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors