···11# maps.bpev.me
2233-This is a proof-of-concept for building offline maps as a PWA, using maplibregl and protomaps.
33+MapsApp is an offline-capable geomap, meant for storing location bookmarks.
4455-This is meant to prove the dataflow for building something like Organic Maps:
55+It is a PWA built on [MapLibre GL](https://maplibre.org) and [PMTiles](https://protomaps.github.io), and serves as a proof-of-concept for the data flow behind something like [Organic Maps](https://organicmaps.app). A world basemap is fetched on first load, users can download regional tiles on demand with additional detail, and everything renders offline after that. Bookmarks, collections, and search history are stored locally.
6677-- A world basemap is downloaded on app load/install
88-- User can download regional maps via the app
99-- Downloaded maps are displayed offline, and overlay the world basemap
77+## Technologies
1081111-## Technologies for App
99+**App**
12101313-- **[civility](https://jsr.io/@civility)**: PWA build system and service worker framework used to bundle the app and manage offline caching
1414-- **[pmtiles](https://protomaps.github.io)**: I settled on pmtiles because the ability to run maplibregl directly from static files simplifies the process on the client
1515-- **[maplibregl](https://maplibre.org)**: a powerful solution that has a defined happy path with pmtiles
1616-- **[deno](https://deno.com)**: I'm just using this for my build system for my other projects, so used it here for my convenience. Could probably make it work in vanilla or a node build system without too much effort.
1111+- **[maplibre-gl](https://maplibre.org)** — vector tile rendering engine
1212+- **[pmtiles](https://protomaps.github.io)** — single-file tile archive format; enables serving maps from static files with no tile server
1313+- **[pmtiles-offline](https://github.com/jtbaker/pmtiles-offline)** — utility for serving pmtiles from local data
1414+- **[nominatim](https://nominatim.org)** - api for geocoding
1515+- **[civility](https://jsr.io/@civility)** — PWA build system, service worker framework, router, and state management
1616+- **[lit](https://lit.dev)** — reactive web components for route views
1717+- **[deno](https://deno.com)** — runtime and toolchain
17181818-### Technologies for Tile Creation
1919+**Tile creation** — see [data/README.md](data/README.md) for the full pipeline.
19202020-- **[geofabrik](https://download.geofabrik.de)**: I don't really want to deal with defining regions, so geofabrik seems like a natural choice; I can use their `oms.pbf` and `.poly` files to help define my download regions in the future
2121-- [**NaturalEarthData**](https://www.naturalearthdata.com): Very fast way to create planet-scale basemaps. I think for greater control, however, I might prefer to build directly from planet.osm for a full product.
2222-- [**ogr2ogr**](https://gdal.org/en/stable/programs/ogr2ogr.html): Basically used just as a way to move from Natural Earth Data to data consumable by tippecanoe
2323-- [**tippecanoe**](https://github.com/felt/tippecanoe): Their docs are [basically the process I used](https://github.com/felt/tippecanoe?tab=readme-ov-file#show-countries-at-low-zoom-levels-but-states-at-higher-zoom-levels) for the basemap tile creation, and I wish I found it way earlier, because it was a pain in the ass without it.
2424-- [**tilemaker**](http://tilemaker.org): Fills similar space as tippecanoe. Seems like it might be easier to deal with filters using it. But using either one of this or tippecanoe would probably be good enough.
2121+## Development
25222626-## Usage
2727-2828-The only prerequisite is to have [deno](https://deno.com) installed.
2323+Running the app requires [Deno](https://deno.com) and [Civility](https://jsr.io/@civility/cli):
29243025```sh
3131-# install deno
3226curl -fsSL https://deno.land/install.sh | sh
3333-3434-# Run App
3535-deno task dev
2727+deno install -gA jsr:@civility/cli --name civ
3628```
37293838-### Things you'd probably want to integrate if taking it further:
3030+Before running the app, you need tiles, built via the [CLI](data/README.md):
39314040-- More of an automated way of creating region tiles
4141-- Scrolling on the map on some zoom levels will expose download links for viewable regions
4242- - Reading user position could also be used to dynamically load maps a user has already downloaded
4343-- Report download progress
4444-- Make sure that region tiles overlay basemap in such a way that the janky guesstimations aren't seen by the user
4545-- The regions on geofabrik are quite heavy; might want to either trim the data, or make even more local maps available.
3232+```sh
3333+deno task data list
3434+# First, download or extract osm files to /data/osm
3535+deno task data download:osm europe/spain
3636+deno task data extract europe/spain --from planet
46374747-## Building the Regional Tiles
4848-4949-I generated the local regional tiles via:
5050-5151-- download `.osm.pbf` from geofabrik
5252-- `tilemaker thailand-latest.osm.pbf thailand.pmtiles`, using `config-openmaptiles.json` and `process-openmaptiles.lua` (I cloned these into scripts/tilemaker)
5353-5454-## Building the Basemap Tiles
5555-5656-Download data the following data from https://www.naturalearthdata.com
3838+# Then build pmtiles
3939+deno task data build europe/spain
4040+```
57415858-- ne_10m_admin_0_boundary_lines_land
5959-- ne_10m_admin_0_countries
4242+The you can then serve or build the app with Civility CLI:
60436144```sh
6262-# Convert the shp files into geojson
6363-# note: you do need the other files to exist alongside the shp files
6464-ogr2ogr -f GeoJSON boundaries.geojson ne_10m_admin_0_boundary_lines_land.shp
6565-ogr2ogr -f GeoJSON countries.geojson ne_10m_admin_0_countries.shp
4545+civ start # Start a server with hot reload
4646+civ build # Builds static files to serve from /www
4747+```
66486767-# process the geojson into mbtiles via tippecanoe
6868-tippecanoe -Z4 -zg -o boundaries.mbtiles --coalesce-densest-as-needed --extend-zooms-if-still-dropping boundaries.geojson
6969-tippecanoe -z3 -zg -o countries.mbtiles --coalesce-densest-as-needed countries.geojson
4949+**Lint and type-check:**
70507171-# Join together the tiles into world tiles
7272-tile-join -o world.mbtiles countries.mbtiles boundaries.mbtiles
7373-7474-#Convert the world file into pmtiles
7575-pmtiles convert world.mbtiles world.pmtiles
5151+```sh
5252+deno task test
7653```
+57-81
data/README.md
···11# Data
2233-This directory holds intermediate files used to generate the `.pmtiles` map tile files served by the `www` app. The output tiles live in `www/static/tiles/`.
33+This directory holds intermediate files used to generate the `.pmtiles` map tile files served by the `www` app. Output tiles are written to `www/static/tiles/`.
4455All tile generation is done via the CLI:
66···99deno task data --help
1010```
11111212-## Tilemaking Pipeline
1212+## Dependencies
1313+1414+The following tools must be installed and on `PATH`:
1515+1616+- [`tilemaker`](https://github.com/systemed/tilemaker) — PBF → PMTiles conversion (`build`, `build:world`)
1717+- [`osmium`](https://osmcode.org/osmium-tool/) — PBF extraction (`extract` command)
1818+1919+## CLI Commands
2020+2121+| Command | Description |
2222+| ------------------------------------------ | ------------------------------------------------- |
2323+| `list [--search <term>]` | Browse available region slugs |
2424+| `download:osm <region> [--force]` | Download `.osm.pbf` from Geofabrik |
2525+| `download:poly [region] [--all] [--force]` | Download `.poly` boundary file(s) |
2626+| `extract <region> --from <source>` | Carve a sub-region from a larger PBF using osmium |
2727+| `build <region>` | Convert `.osm.pbf` → `.pmtiles` using tilemaker |
2828+| `build:world` | Build world-scale basemap tiles |
2929+| `clean --region <slug> \| --all` | Remove intermediate files |
3030+3131+## World Tiles
3232+3333+The world basemap is built from OSM data using a tilemaker pipeline, sourced from a full planet PBF. Place `planet-latest.osm.pbf` in `data/osm/` and run:
3434+3535+```sh
3636+deno task data build:world [--maxzoom <5|7|9>] # default maxzoom: 7
3737+```
3838+3939+Output is written to `www/static/tiles/world_z<maxzoom>.pmtiles`. The config and Lua script used are separate from the regional ones:
4040+4141+- `data/cli/shared/tilemaker/config.world.json` — layer definitions for the world overview
4242+- `data/cli/shared/tilemaker/process.world.lua` — feature processing logic
4343+4444+Note: this takes a super long time to run.
4545+4646+## Regional Tiles
13471414-Tiles are generated from [OpenStreetMap](https://www.openstreetmap.org/) data. The pipeline has three stages:
4848+Regional tiles follow a three-stage pipeline:
15491650```
1717-Geofabrik (download) → osmium (extract) → tilemaker (build) → .pmtiles
5151+Geofabrik (download) → osmium (extract; optional) → tilemaker (build) → .pmtiles
1852```
19532054### 1. Download
21552222-Regional `.osm.pbf` data files are sourced from [Geofabrik](https://download.geofabrik.de/). All available regions are listed in [`../data/geofabrik/regions.txt`](../data/geofabrik/regions.txt).
5656+Regional `.osm.pbf` data and `.poly` boundary files are sourced from [Geofabrik](https://download.geofabrik.de/). Region slugs match Geofabrik's hierarchy (e.g. `asia/japan/kansai`, `europe/monaco`).
23572424-`.poly` boundary files for all regions are already committed to `data/poly/` and do not need to be downloaded. If you need to refresh them, use:
5858+`.poly` files for all 499 Geofabrik regions are already committed to `data/poly/` and don't need to be refreshed under normal circumstances. OSM `.pbf` files are gitignored due to size and must be downloaded as needed.
25592660```sh
2727-deno task data download:osm europe/monaco # OSM map data
2828-deno task data download:poly europe/monaco # boundary polygon (already present)
6161+deno task data download:osm europe/monaco
6262+deno task data download:poly europe/monaco # already present; only needed to refresh
2963```
3030-3131-Downloaded OSM files are saved to `data/osm/<region>-latest.osm.pbf` (gitignored due to size).
32643365### 2. Extract (optional)
34663535-If you already have a large regional PBF (e.g. an entire continent), you can carve out a smaller region using `osmium` rather than downloading from Geofabrik again:
6767+If you already have a large regional PBF, you can carve out a sub-region using `osmium` rather than downloading from Geofabrik again:
36683769```sh
3870deno task data extract asia/taiwan --from asia
3971```
40724141-This requires both the source PBF and the target `.poly` file to already be present.
7373+This requires both the source PBF and the target `.poly` file to be present.
42744375### 3. Build
44764545-Convert the `.osm.pbf` to a `.pmtiles` file using `tilemaker`:
7777+Convert `.osm.pbf` to `.pmtiles` using `tilemaker`:
46784779```sh
4880deno task data build europe/monaco
4981```
50825151-Output is written to `www/static/tiles/<region-name>.pmtiles`. The tilemaker config and processing script used are:
8383+Output is written to `www/static/tiles/<region-name>.pmtiles`. The tilemaker config and Lua processing script are:
52845353-- [`../data/tilemaker/config.json`](../data/tilemaker/config.json) — layer definitions (OpenMapTiles schema, zoom 0–14)
5454-- [`../data/tilemaker/process.lua`](../data/tilemaker/process.lua) — feature processing logic
8585+- `data/cli/shared/tilemaker/config.json` — layer definitions (OpenMapTiles schema, zoom 0–14)
8686+- `data/cli/shared/tilemaker/process.lua` — feature processing logic
55875688## Common Workflows
57895858-### Small country (direct download)
9090+**Small country — direct download:**
59916092```sh
6193deno task data download:osm europe/monaco
6294deno task data build europe/monaco
6395```
64966565-### Country carved from a larger extract
9797+**Country carved from a larger extract:**
66986799```sh
6868-deno task data download:osm asia # large, but reusable for multiple countries
100100+deno task data download:osm asia # large, but reusable across multiple countries
69101deno task data extract asia/taiwan --from asia
70102deno task data build asia/taiwan
71103```
721047373-## CLI Commands
7474-7575-| Command | Description |
7676-| ------------------------------------------ | ------------------------------------ |
7777-| `list [--search <term>]` | Browse available region slugs |
7878-| `download:osm <region> [--force]` | Download `.osm.pbf` from Geofabrik |
7979-| `download:poly [region] [--all] [--force]` | Download `.poly` boundary file(s) |
8080-| `extract <region> --from <source>` | Carve a sub-region from a larger PBF |
8181-| `build <region>` | Run tilemaker → `.pmtiles` |
8282-| `clean --region <slug> \| --all` | Remove intermediate files |
8383-8484-## Dependencies
8585-8686-The following tools must be installed and available on `PATH`:
8787-8888-- [`osmium`](https://osmcode.org/osmium-tool/) — OSM data processing (`extract` command)
8989-- [`tilemaker`](https://github.com/systemed/tilemaker) — PBF to vector tile conversion (`build` command)
9090-91105## Directory Structure
9210693107```
94108data/
9595- osm/ Downloaded/extracted .osm.pbf files (gitignored — large)
9696- asia/
9797- taiwan-latest.osm.pbf
9898- europe/
9999- monaco-latest.osm.pbf
100100- poly/ Boundary polygon files (committed)
101101- asia/
102102- taiwan.poly
103103- europe/
104104- monaco.poly
105105- cli/ The CLI source code
109109+ osm/ Downloaded/extracted .osm.pbf files (gitignored)
110110+ poly/ Boundary .poly files for all 499 Geofabrik regions (committed)
111111+ cli/ CLI source code
106112```
107113108108-`data/osm/` is gitignored due to file size. Use `deno task data download:osm` to fetch OSM data as needed. `data/poly/` is committed — all 499 Geofabrik regions are already present.
114114+## External Resources
109115110110-## Resources
111111-112112-### Shapefiles
113113-114114-- https://osmdata.openstreetmap.de/data/water-polygons.html
115115-116116-### geofabrik
117117-118118-Basically, to fully realize a project like this, we will need scripts to organize the different downloadable regions, and also to find the urls for us to download. This doesn't include region naming/localization, so maybe need to figure that out later.
119119-120120-The files we need are:
121121-122122-1. `[insert-region]-latest.osm.pbf`: map data
123123-2. `[insert-region].poly`: the borders of each region
124124-125125-### tilemaker
126126-127127-We want tilemaker to create tiles that are compatible with our protomaps styles. Basically, you just need these files in the directory when you need to process your pbf:
128128-129129-```
130130-tilemaker monaco-latest.osm.pbf monaco.pmtiles
131131-```
132132-133133-# World Tiles
134134-135135-Three output tiers, all generated from the same filtered planet PBF:
136136-137137-| File | Zoom | Approx size | What's in it |
138138-| ---------------- | ----- | ----------- | --------------------------------------------- |
139139-| world_z5.pmtiles | z0–z5 | ~5–10 MB | Ocean, country fills, country names, capitals |
140140-| world_z7.pmtiles | z0–z7 | ~25–50 MB | + major cities, state boundaries, main rivers |
141141-| world_z9.pmtiles | z0–z9 | ~100–180 MB | + towns, all highways, detailed water |
116116+- **[Geofabrik](https://download.geofabrik.de)** — regional OSM extracts; provides both `.osm.pbf` and `.poly` files
117117+- **[OpenStreetMap](https://planet.openstreetmap.org)** — planet PBF source for world basemap tiles