- osm-data.mjs: add 'roads' Overpass query to tulsa area — major road classes (motorway/trunk/primary/secondary/tertiary) as LineString geometry - MapView.svelte: render POIs that have a bearing as a rotated arrow divIcon marker pointing in the compass heading; other POIs stay as circleMarkers. Adds .dir-arrow CSS. - scripts/test.mjs: adapt street-name search assertions (roads now match address queries legitimately) and add an expectAll helper - static/data: tulsa.geojson now includes ~20k road features (11MB)
Navigator
A custom web application that serves OpenStreetMap (OSM) data. It has a build process that:
- Pulls OSM data from the Overpass API for configurable geographic areas and feature types.
- Merges hand-authored custom layers — zones (polygons) and points of
interest — from
scripts/custom-layers.mjs. - Converts everything to static GeoJSON files.
- 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
- SvelteKit (Svelte 5, runes mode)
- @sveltejs/adapter-static — outputs a static site
- Leaflet — interactive map on the frontend
- Overpass API — OSM data source
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 |
Runs scripts/osm-data.mjs to fetch OSM data → static/data/ |
npm run build |
build:data followed by the 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.
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 legendlayerType—'poi'(point),'zone'(polygon), or'path'(LineString)color— hex color used for markers/fill/linesfeatures— 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 popupbearing— compass direction the POI faces; accepts a compass point ('N','NNE','NE', ...) or a numeric bearing in degreesfov— 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.
Search
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 —
st→street,rd→road,ave→avenue,n→north, etc.), so abbreviated or partial addresses resolve correctly. - Uses substring + Levenshtein edit-distance tolerance, so typos and near-
matches still hit (e.g.
philbrickfindsPhilbrook). - Ranks name matches above address matches above description matches.
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 checksnpm test— data-integrity + fuzzy-search regression suite against committed datanpm run build:static— static site build (uses committedstatic/data)
Requires a registered Gitea Actions runner (label host).
Repository
Hosted on Gitea: https://gitea.thecookiejar.me/hermes-explorigin/navigator