2026-08-10 14:22:31 +00:00

Navigator

A custom web application that serves OpenStreetMap (OSM) data. It has a build process that:

  1. Pulls OSM data from the Overpass API for configurable geographic areas and feature types.
  2. Merges hand-authored custom layers — zones (polygons) and points of interest — from scripts/custom-layers.mjs.
  3. Converts everything to static GeoJSON files.
  4. Builds a SvelteKit frontend (with the static adapter) into a plain static site that renders each data source as a toggleable layer on an interactive Leaflet map.

The final output in build/ is a fully self-contained static site — no server runtime required. You can serve it with any static HTTP server (nginx, Apache, python -m http.server, S3/Cloudflare Pages, etc.).

Built With

Quick Start

npm install

# Fetch & convert OSM data into static/data/*.geojson
npm run build:data

# Full production build: (fetch data) + (static site) -> build/
npm run build

# Preview the production build locally
npm run preview

Development

npm run dev         # Vite dev server (your data in static/data is served too)
npm run check       # Type + Svelte checks

Build Process

Command What it does
npm run build:data Overpass API fetch (area POIs/roads) via scripts/osm-data.mjsstatic/data/
npm run build:dump OSM data-dump ingestion + routing graph via scripts/osm-dump.mjs
npm run build build:data + build:dump + SvelteKit static build → build/
npm run preview Serves the build/ output locally

Data layout

static/data/
├── _index.json              # Manifest of all built layers (drives the UI)
├── <area>.geojson           # One FeatureCollection per Overpass area
└── custom/
    └── <layer>.geojson      # One FeatureCollection per custom zone/POI layer

The frontend loads /data/_index.json, then fetches each layer's GeoJSON and renders it as a toggleable overlay with a legend (top-right). Every layer can be shown/hidden independently. By default no layers are shown on load — toggle the ones you want from the legend.

OSM Data-Dump Ingestion (osm-dump.mjs)

As an alternative to the live Overpass API, you can ingest a regional .osm.pbf data dump (the files Geofabrik, BBBike, etc. publish). This is great for offline builds, large areas, and for building a routable road graph (the Overpass path only fetches features).

scripts/osm-dump.mjs:

  1. Downloads a configured .osm.pbf URL (cached under scripts/.cache/, so repeated runs skip the network).
  2. Stream-parses it with osm-pbf-parser.
  3. Clips to a bounding box and extracts:
    • POI features (from poiTags) as points → static/data/<id>-poi.geojson
    • A routable road graph (coords + weighted adj) from road ways → static/data/<id>-graph.json
  4. Registers a category: 'dump' layer in the manifest with graphPath.

Configure sources in the DUMPS array at the top of scripts/osm-dump.mjs (url, bbox, poiTags, graphHighways). The default source is Geofabrik's Oklahoma extract clipped to the Tulsa bbox.

npm run build:dump                 # build all dump sources
node scripts/osm-dump.mjs --list   # list configured sources

The downloaded .pbf lives in scripts/.cache/ (git-ignored); only the generated static/data/*.geojson and -graph.json outputs are committed.

Configuring Areas & Queries

Edit scripts/osm-data.mjs — the AREAS object describes each area:

  • bbox[south, west, north, east] in decimal degrees.
  • queries — a map of name → Overpass QL snippet. Use {{bbox}} as the placeholder for the area's bounding box.
  • The script falls back across multiple public Overpass mirrors if one is overloaded, and is polite to the API (short delay between queries).

Example:

parks: `
  way["leisure"="park"]({{bbox}});
  out center tags geom;
`

By default the tulsa area also fetches major roads (highway = motorway/trunk/primary/secondary/tertiary) as LineStrings, so the map shows road network geometry alongside the POI/zones. (Tiny local roads like residential/service are excluded to keep the output file size reasonable.) Adjust the roads query in scripts/osm-data.mjs to include or exclude road classes.

Note: the Overpass API is sometimes rate-limited; npm run build:data may need to be re-run if a query returns 5xx. The committed static/data/ already contains a full road dataset.

List configured queries with npm run data -- --queries, or build a single area with npm run data -- --area=tulsa.

Custom Layers (Zones, Points of Interest & Paths)

Besides Overpass-fetched data, you can define your own layers by hand in scripts/custom-layers.mjs. It ships with several example layers (my-places POIs, districts zones, trails path, and rogers-county local POIs) that you can extend or replace.

Each layer has:

  • id — unique slug (becomes the output filename)
  • name — label shown in the UI legend
  • layerType'poi' (point), 'zone' (polygon), or 'path' (LineString)
  • color — hex color used for markers/fill/lines
  • features — array of feature objects

POI (point) — optional address, bearing, and fov fields:

{
  name: 'Gathering Place',
  desc: 'Riverside park',
  address: '2650 S John Williams Way E, Tulsa, OK 74114',
  bearing: 'SE',   // compass point (N, NNE, NE, ...) OR numeric degrees (0-360)
  fov: 140,        // field of view in degrees (optional)
  lon: -95.9791,
  lat: 36.1098
}
  • address — street address shown in the popup
  • bearing — compass direction the POI faces; accepts a compass point ('N', 'NNE', 'NE', ...) or a numeric bearing in degrees
  • fov — field of view in degrees

The popup renders these as dedicated rows (Facing / Field of view), and a directional arrow is drawn on the map at each POI that has a bearing, rotated to the compass heading it faces.

Zone (polygon — the ring is closed for you):

{
  name: 'Downtown Tulsa',
  desc: 'Rough downtown core',
  polygon: [
    [-96.0000, 36.1300],
    [-95.9900, 36.1300],
    [-95.9900, 36.1600],
    [-96.0000, 36.1600]
  ]
}

Path / route (LineString — an ordered list of points like a GPX track):

{
  name: 'Claremore Lake Loop',
  desc: 'Example walking route around the lake',
  line: [
    [-95.5649, 36.3415],
    [-95.5700, 36.3430],
    [-95.5750, 36.3420],
    [-95.5720, 36.3360]
  ]
}

Paths render as dashed polylines and are searchable by name.

Coordinates are [longitude, latitude] (lon first, like GeoJSON). The file also includes a copy-paste TEMPLATE block for adding new layers. After editing, run npm run build:data (or npm run build) to regenerate the static data.

The webapp includes a fuzzy search in the top bar. It matches your POIs, zones, and paths by name or address — and, as a fallback, queries the Nominatim OSM geocoder so you can also search for any location by name (cities, landmarks, addresses anywhere). A "Places (online)" section appears beneath the local matches; selecting a result pans/zooms the map to it (fit-to-bounds for places with a bounding box).

The matcher:

  • Normalizes text (case, punctuation, and common address words — ststreet, rdroad, aveavenue, nnorth, etc.), so abbreviated or partial addresses resolve correctly.
  • Uses substring + Levenshtein edit-distance tolerance, so typos and near- matches still hit (e.g. philbrick finds Philbrook).
  • Ranks name matches above address matches above description matches.

Preferences & Units

Click ⚙ Preferences in the top bar to choose the unit systemMetric or Imperial. The choice is saved to localStorage, so it persists across reloads and is shared by the Directions panel.

Units are auto-selected within the chosen system: m/ft under a kilometer/mile, otherwise km/mi (e.g. 820 m, 1.9 km; 900 ft, 1.2 mi).

Directions & Routing

When a dump layer is present (it carries the routable graph), a Directions button appears in the top bar. Opening it lets you:

  • Set start (A) and end (B) by clicking the map.
  • Avoid certain POI categories along the route (schools, fire stations, fuel stations, parking, restaurants, …).
  • Compute a route and draw it on the map.
  • Choose a unit system (Metric or Imperial; distances auto-switch between m/ft and km/mi at the km/mile boundary), plus a travel mode (Drive or Walk).
  • See the route's distance and an estimated travel time (computed from the mode's average speed).
  • Turn-by-turn directions (Head north, Turn right, Arrive at destination, …) are generated for Drive mode and listed beneath the result; a live progress bar shows while the route computes.
  • Robustness: if A/B are far from the routable road network, or the destination is disconnected / fully blocked by avoided categories, the app reports why and still draws the closest reachable portion of a route (flagged as a partial path) instead of failing silently.

Routing is done entirely in the browser (src/lib/routing.ts): an A*-search over the graph built from the OSM dump. Point A/B are snapped to the nearest graph node. When you enable a category to avoid, edges within a radius of POIs in that category are heavily penalized (or blocked) so the route routes around them.

import { route } from '$lib/routing';
const r = route(graph, [latA, lonB], [latB, lonB], pois, [
  { category: 'amenity', value: 'school', radiusM: 300, block: true }
]);
// r.found, r.path ([[lat,lon],...]), r.distanceM
// r.partial (best-effort partial route when the destination is unreachable),
// r.fromSnapM / r.toSnapM (distance from A/B to nearest road),
// r.reason (human explanation when no full route exists),
// r.turns (turn-by-turn steps for driving mode)

Deployment

The build/ directory is entirely static. Serve it directly:

cd build
python3 -m http.server 8080
# or: nginx -s  listen 8080; root /path/to/build;  (with a fallback to index.html)

Because the site uses a static adapter with prerendering, everything can be hosted on any static file host or CDN.

Continuous Integration

Gitea Actions runs tests on every commit/push (and on pull requests) via .gitea/workflows/ci.yml. It runs offline (no Overpass/Nominatim calls) so it won't trip API rate limits:

  • npm run check — Svelte/TypeScript checks
  • npm test — data-integrity + fuzzy-search regression suite against committed data
  • npm run build:static — static site build (uses committed static/data)

Requires a registered Gitea Actions runner (label host).

Repository

Hosted on Gitea: https://gitea.thecookiejar.me/exlim/navigator

Description
Software project repository
Readme 14 MiB
Languages
Svelte 41.3%
JavaScript 30%
TypeScript 28.5%
HTML 0.2%