All checks were successful
CI / test-and-build (push) Successful in 42s
302 lines
11 KiB
Markdown
302 lines
11 KiB
Markdown
# Navigator
|
|
|
|
A custom web application that serves **OpenStreetMap (OSM) data**. It has a build
|
|
process that:
|
|
|
|
1. **Pulls** OSM data from the [Overpass API](https://overpass-api.de/) 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
|
|
|
|
- [SvelteKit](https://svelte.dev/kit) (Svelte 5, runes mode)
|
|
- [@sveltejs/adapter-static](https://kit.svelte.dev/docs/adapter-static) — outputs a static site
|
|
- [Leaflet](https://leafletjs.com/) — interactive map on the frontend
|
|
- [Overpass API](https://overpass-api.de/) — OSM data source
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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.mjs` → `static/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.
|
|
|
|
```bash
|
|
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:
|
|
|
|
```js
|
|
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:
|
|
|
|
```js
|
|
{
|
|
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):
|
|
|
|
```js
|
|
{
|
|
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):
|
|
|
|
```js
|
|
{
|
|
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](https://nominatim.openstreetmap.org/) 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. `philbrick` finds `Philbrook`).
|
|
- Ranks name matches above address matches above description matches.
|
|
|
|
## Preferences
|
|
|
|
Click **⚙ Preferences** in the top bar:
|
|
|
|
- **Unit system** — **Metric** 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 Points** — **Export / 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 (*Head north*,
|
|
*Turn right*, *Arrive at destination*, ...) for **Drive** mode, plus distance,
|
|
estimated travel time, and the drawn route.
|
|
|
|
## 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:
|
|
|
|
```bash
|
|
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 |