hermes-explorigin fb6ad3eb0c
All checks were successful
CI / test-and-build (push) Successful in 48s
Fix: resolve data paths against app base for subdirectory deploys
The static app is served at /maps/ but the data manifest and graph/poi/layer
fetches used absolute /data/... paths, which resolved to the server root and
failed. Add src/lib/base.ts exporting upath() that prefixes a path with the
runtime-detected base (mirroring SvelteKit's 'base: new URL(".", location)'),
and apply it to the manifest, layer, graph and POI fetches. Works at both
root and any sub-path.
2026-08-16 14:39:58 +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

Click ⚙ Preferences in the top bar:

  • Unit systemMetric or Imperial (saved to localStorage). 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).
  • Saved PointsExport / Import your browser-local saved points as a JSON file (see below). Imported points are merged (deduplicated by id); existing points are kept.

Directions & Routing

Everything starts from the map. Clicking one spot opens a context dialog; no click ever saves or routes automatically.

  • Clicking an existing point (a POI/zone/path, or a Saved Point) shows its info and offers "Directions from here" - which sets it as the origin and opens the destination picker - plus "Directions to here" once an origin is set.
  • Clicking empty space shows a "Save point..." form (with an optional label) to save it as a browser-local Saved Point, or the same "Directions from here" action.

From the opened Directions panel:

  1. Origin is already set (from "Directions from here"). Pick a destination - by clicking another point (and choosing "Directions to here"), searching a POI (local or online via Nominatim), or choosing a Saved Point.
  2. Calculate route enables once both points are set. The computation runs in a Web Worker (off the UI thread) with a live progress bar.
  3. On completion the panel shows the turn-by-turn directions for Drive mode, plus distance, estimated travel time, and the drawn route.

Turn-by-turn is street-aware: the routing graph stores road names, so steps read like "Head north on Pine St", "Turn right onto 21st St", "Sharp left onto OK-20" (falling back to a bare compass/turn label when a way is unnamed). Tiny micro-jogs on a straight street are merged so you don't get noisy phantom turns. The graph is directed: one-way streets and roundabouts are respected, so routes don't travel the wrong way down a one-way road.

Saved Points (browser-local)

Clicking an empty spot on the map opens a context dialog with a "Save point..." button (plus an optional label). Saving creates a Saved Point — a location you want to remember. Saved points are stored only in your browser's localStorage and shown as a toggleable "Saved Points" layer in the legend, with a distinct purple pin. They can be used as route origins or destinations.

Because they live only in the browser, Export / Import (in ⚙ Preferences) lets you back them up as a JSON file and restore them (merged, deduplicated by id).

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%