From 4dce43f66bc00440a19677f557b309d12effa6ca Mon Sep 17 00:00:00 2001 From: "Nathaniel V. KELSO" Date: Thu, 28 Mar 2024 14:57:27 -0700 Subject: [PATCH 01/88] add landcover layer docs (#33) * ignore hidden mac files * [docs] add new landcover layer for #154 * flesh out landuse kinds and basic layer description * add sport tag, and deprecation note about other OSM tag > key mappings without listing them all * add natural, places, pois layers * roads, transit, water docs --- .gitignore | 1 + basemaps/layers.md | 97 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 84 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index b733034..7a7021f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .vitepress/cache .vitepress/dist +.DS_Store diff --git a/basemaps/layers.md b/basemaps/layers.md index 433dda0..151aad6 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -48,14 +48,39 @@ Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preproce | ----------- | :-------: | -----------: | | `pmap:kind` | `earth` | | +## landcover + +Polygons from the Daylight distribution's [landcover](https://daylightmap.org/2023/10/11/landcover.html) theme, for z0-z7. + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `barren`, `farmland`, `forest`, `glacier`, `grassland`, `scrub`, `urban_area` | | + +_NOTE: It's recommended to pair with **natural** layer polygons from OpenStreetMap at mid- and high-zooms._ + ## landuse -`kind` +Polygons from OpenStreetMap, from a curated subset of aeroway, amenity, area:aeroway, boundary, highway, landuse, leisure, man_made, natural, place, railway, tourism tags, for all zooms. + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `aerodrome`, `attraction`, `beach`, `cafe`, `camp_site`, `cemetery`, `college`, `commercial`, `dog_park`, `farmland`, `farmyard`, `footway`, `forest`, `garden`, `golf_course`, `grass`, `grocery`, `hospital`, `hotel`, `industrial`, `kindergarten`, `library`, `marina`, `military`, `national_park`, `nature_reserve`, `neighbourhood`, `orchard`, `other`, `park`, `pedestrian`, `pier`, `pitch`, `platform`, `playground`, `post_office`, `protected_area`, `railway`, `recreation_ground`, `residential`, `runway`, `school`, `stadium`, `supermarket`, `taxiway`, `townhall`, `university`, `zoo` | | +| `sport` | string | Which sports are played on a pitch. | + +_NOTE: Additional keys are available for each original OSM tags (when available), but those will be deprecated in the next major version so should not be used for styling._ ## natural +Polygons from OpenStreetMap, from a curated subset of natural and landuse tags, for all zooms. + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `wood`, `glacier`, `grass`, `scrub`, `sand`, `wetland`, `bare_rock`, `forest`, `meadow`, `grass` | | + +_NOTE: It's recommended to pair with **landcover** layer polygons from Daylight at low-zooms._ + ## physical_line ::: warning @@ -68,35 +93,79 @@ physical_line will be deprecated in v4.0. physical_point will be deprecated in v4.0. ::: - ## places -`kind` +Points from OpenStreetMap and Natural Earth, from a curated subset of place tags, for all zooms. + + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | +| `pmap:kind_detail` | `city`, `country`, `hamlet`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | +| `capital` | string | | +| `population` | int | | +| `pmap:population_rank` | int | | +| `wikidata` | string | | + +_NOTE: Additional keys are available for each original OSM tags (when available), but those will be deprecated in the next major version so should not be used for styling._ ## pois -`kind` +Points from OpenStreetMap, from a curated subset of aeroway, amenity, attraction, boundary, craft, highway, historic, landuse, leisure, natural, railway, shop, tourism tags, for all zooms. + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `aerodrome`, `adult_gaming_centre`, `airfield`, `alpine_hut`, `amusement_ride`, `animal`, `art`, `artwork`, `atm`, `attraction`, `atv`, `baby_hatch`, `bakery`, `bbq`, `beauty`, `bed_and_breakfast`, `bench`, `bicycle_parking`, `bicycle_rental`, `bicycle_repair_station`, `boat_storage`, `bookmaker`, `books`, `bureau_de_change`, `bus_stop`, `butcher`, `cafe`, `camp_site`, `car_parts`, `car_rental`, `car_repair`, `car_sharing`, `car_wash`, `car`, `carousel`, `cemetery`, `chalet`, `charging_station`, `childcare`, `clinic`, `clothes`, `college`, `computer`, `convenience`, `customs`, `dentist`, `district`, `doctors`, `dog_park`, `drinking_water`, `emergency_phone`, `fashion`, `firepit`, `fishing`, `florist`, `forest`, `fuel`, `gambling`, `garden_centre`, `gift`, `golf_course`, `golf`, `greengrocer`, `grocery`, `guest_house`, `hairdresser`, `hanami`, `harbourmaster`, `hifi`, `hospital`, `hostel`, `hotel`, `hunting_stand`, `information`, `jewelry`, `karaoke_box`, `karaoke`, `landmark`, `library`, `life_ring`, `lottery`, `marina`, `maze`, `memorial`, `military`, `mobile_phone`, `money_transfer`, `motorcycle_parking`, `motorcycle`, `national_park`, `naval_base`, `newsagent`, `optician`, `park`, `parking`, `perfumery`, `picnic_site`, `picnic_table`, `pitch`, `playground`, `post_box`, `post_office`, `ranger_station`, `recycling`, `roller_coaster`, `sanitary_dump_station`, `school`, `scuba_diving`, `shelter`, `ship_chandler`, `shower`, `slipway`, `snowmobile`, `social_facility`, `stadium`, `stationery`, `studio`, `summer_toboggan`, `supermarket`, `swimming_area`, `taxi`, `telephone`, `tobacco`, `toilets`, `townhall`, `trail_riding_station`, `travel_agency`, `university`, `viewpoint`, `waste_basket`, `waste_disposal`, `water_point`, `water_slide`, `watering_place`, `wayside_cross`, `wilderness_hut` | | +| `cuisine` | string | | +| `religion` | string | | +| `sport` | string | | +| `iata` | string | | + +_NOTE: The list of kind values is not comprehensive as some raw OSM tag values are passed through in the current version._ ## roads -`kind` +Lines from OpenStreetMap, from a curated subset of highway tags, for mid- and high-zooms. + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `highway`, `major_road`, `medium_road`, `minor_road`, `path`, `other` | | +| `pmap:kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway` | | +| `ref` | string | | +| `shield_text_length` | int | | +| `network` | string | | +| `layer` | int | | +| `oneway` | string | | +| `service` | string | | +| `pmap:link` | int | | +| `pmap:level` | `-1`, `0`, `1` | | + ## transit -`kind` +Lines from OpenStreetMap, from a curated subset of railway, aerialway, man_made, route, and aeroway tags, for mid- and high-zooms. + +| Key | Values | Description | +| ----------- | :-------: | -----------: | +| `pmap:kind` | `aerialway`, `cable_car`, `crossover`, `ferry`, `pier`, `rail`, `runway`, `siding`, `taxiway`, `yard` | | +| `pmap:kind_detail` | `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `railway`, `subway`, `tram` | | +| `ref` | string | | +| `network` | string | | +| `layer` | int | | +| `route` | string | | +| `service` | string | | ## water Polygons from the Natural Earth 50m `lakes` and `ocean` themes for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. - | Key | Values | Description | | ------------------ | :------------------------------------------: | -----------: | | `pmap:kind` | `water`, `lake`, `playa`, `ocean`, `other` | | -| `pmap:kind_detail` | | | -| `reservoir` | | | -| `alkaline` | | | -| `intermittent` | | | -| `bridge` | | | -| `tunnel` | | | -| `layer` | | | +| `pmap:kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | +| `reservoir` | boolean | | +| `alkaline` | boolean | | +| `intermittent` | boolean | | +| `bridge` | string | | +| `tunnel` | string | | +| `layer` | int | | From a34b3e674b601d535aa1835343e7f792c3a5c9cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 14 Apr 2024 07:28:12 -0700 Subject: [PATCH 02/88] Bump vite from 4.5.2 to 4.5.3 (#35) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.2 to 4.5.3. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v4.5.3/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v4.5.3/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83579a0..5008d61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1581,9 +1581,9 @@ } }, "node_modules/vite": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.2.tgz", - "integrity": "sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", + "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", From 35562e98fd629f15f4d43240d34806f3a046ed1e Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 3 May 2024 09:10:47 +0800 Subject: [PATCH 03/88] more details on pmtiles serve (#36) --- pmtiles/cli.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pmtiles/cli.md b/pmtiles/cli.md index e43a5a0..b133818 100644 --- a/pmtiles/cli.md +++ b/pmtiles/cli.md @@ -76,9 +76,13 @@ Options: ```bash pmtiles serve . +pmtiles serve . --bucket=https://example.com +pmtiles serve / --bucket=s3://BUCKET_NAME ``` -Run a Z/X/Y server for a directory or bucket of archives. +Expose Z/X/Y tile URLS, e.g. `/mymap/{z}/{x}/{y}.mvt`, for a directory or bucket of archives. **Requests for the raw file e.g. `mymap.pmtiles` will not work.** + +A Z/X/Y URL like is directly supported by web and native clients such as [MapLibre](http://maplibre.org), without needing the PMTiles client library. Using `pmtiles serve` this way also allows serving public Z/X/Y traffic from private storage buckets. ### convert From 18952f6ed2922cb2ae4f1635b8638e6a6da3e3a2 Mon Sep 17 00:00:00 2001 From: Anders Barfod Date: Sat, 25 May 2024 06:03:48 +0200 Subject: [PATCH 04/88] Added examples on tippecanoe tile-join and ogr2ogr pmtiles conversion (#37) * Add node_modules to .gitignore * Added examples on tippecanoe tile-join and ogr2ogr pmtiles conversion --------- Co-authored-by: Anders Barfod --- .gitignore | 2 ++ pmtiles/create.md | 17 +++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.gitignore b/.gitignore index 7a7021f..cfee3ce 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ .vitepress/cache .vitepress/dist .DS_Store + +node_modules \ No newline at end of file diff --git a/pmtiles/create.md b/pmtiles/create.md index 54876f5..c8fad84 100644 --- a/pmtiles/create.md +++ b/pmtiles/create.md @@ -17,6 +17,13 @@ ogr2ogr -t_srs EPSG:4326 cb_2018_us_zcta510_500k.json cb_2018_us_zcta510_500k.sh tippecanoe -zg --projection=EPSG:4326 -o cb_2018_us_zcta510_500k_nolimit.pmtiles -l zcta cb_2018_us_zcta510_500k.json ``` +To merge multiple pmtiles files into a single file use [`tile-join` tool](https://github.com/felt/tippecanoe?tab=readme-ov-file#tile-join), which is shipped with Tippecanoe + +```sh +# Merge all PMTiles files in current folder into single file +tile-join -o merged.pmtiles *.pmtiles +``` + ## MBTiles the [`pmtiles` command line tool](/pmtiles/cli) converts MBTiles to PMTiles with this command: @@ -41,6 +48,16 @@ pmtiles convert output.mbtiles output.pmtiles GDAL has native support for PMTiles starting with version 3.8.0 (2023-11-13), see [gdal.org/drivers/vector/pmtiles](https://gdal.org/drivers/vector/pmtiles.html) for details. +GDAL's [`ogr2ogr`](https://gdal.org/programs/ogr2ogr.html#ogr2ogr) tool supports a wide range of formats as input for creating PMTiles. Below is examples of generating PMTiles from a shape file and based on multiple PostgreSQL/Postgis tables. + +```sh +# Convert shapefile to to pmtiles +ogr2ogr -dsco MINZOOM=10 -dsco MAXZOOM=20 -f "PMTiles" filename.pmtiles my_shapes.shp + +# Merge all PostgreSQL/PostGIS tables in a schema into a single PMTiles file. +ogr2ogr -dsco MINZOOM=0 -dsco MAXZOOM=22 -f "PMTiles" filename.pmtiles "PG:host=my_host port=my_port dbname=my_database user=my_user password=my_password schemas=my_schema" +``` + ## Other ::: warning From fe33efaa04cca7cec05d065ad2bb91cd83e9b4c3 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 24 May 2024 22:19:30 -0700 Subject: [PATCH 05/88] more details on creating PMTiles with ogr2ogr [#37] (#38) --- pmtiles/create.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pmtiles/create.md b/pmtiles/create.md index c8fad84..f1264ac 100644 --- a/pmtiles/create.md +++ b/pmtiles/create.md @@ -48,16 +48,20 @@ pmtiles convert output.mbtiles output.pmtiles GDAL has native support for PMTiles starting with version 3.8.0 (2023-11-13), see [gdal.org/drivers/vector/pmtiles](https://gdal.org/drivers/vector/pmtiles.html) for details. -GDAL's [`ogr2ogr`](https://gdal.org/programs/ogr2ogr.html#ogr2ogr) tool supports a wide range of formats as input for creating PMTiles. Below is examples of generating PMTiles from a shape file and based on multiple PostgreSQL/Postgis tables. +**Using ogr2ogr to create vector PMTiles is recommended only for smaller datasets: the [tippecanoe](#tippecanoe) tool creates much more efficient overview tiles.** + +GDAL's [`ogr2ogr`](https://gdal.org/programs/ogr2ogr.html#ogr2ogr) tool supports a wide range of formats as input for creating PMTiles. Below are examples of generating PMTiles from a Shapefile or multiple PostgreSQL/PostGIS tables. ```sh # Convert shapefile to to pmtiles -ogr2ogr -dsco MINZOOM=10 -dsco MAXZOOM=20 -f "PMTiles" filename.pmtiles my_shapes.shp +ogr2ogr -dsco MINZOOM=10 -dsco MAXZOOM=15 -f "PMTiles" filename.pmtiles my_shapes.shp # Merge all PostgreSQL/PostGIS tables in a schema into a single PMTiles file. -ogr2ogr -dsco MINZOOM=0 -dsco MAXZOOM=22 -f "PMTiles" filename.pmtiles "PG:host=my_host port=my_port dbname=my_database user=my_user password=my_password schemas=my_schema" +ogr2ogr -dsco MINZOOM=0 -dsco MAXZOOM=15 -f "PMTiles" filename.pmtiles "PG:host=my_host port=my_port dbname=my_database user=my_user password=my_password schemas=my_schema" ``` +* `MAXZOOM=15` is sufficient for street-level mapping. Choosing less detail with a lower `MAXZOOM` will reduce the size of the final file. + ## Other ::: warning From d74b7e5a63995f497297394f6de0abd9aeb17685 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Sat, 8 Jun 2024 11:39:37 -0600 Subject: [PATCH 06/88] new pmtiles.io URLs (#40) --- deploy/aws.md | 2 +- deploy/cloudflare.md | 2 +- pmtiles/index.md | 4 ++-- pmtiles/openlayers.md | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deploy/aws.md b/deploy/aws.md index 86f708f..50556a4 100644 --- a/deploy/aws.md +++ b/deploy/aws.md @@ -33,7 +33,7 @@ outline: deep * On the Configuration tab, choose **Environment Variables** > **Edit**. * set `BUCKET` to your unique **bucket name** from Step 1. * set `PUBLIC_HOSTNAME` to the **public custom domain name you'll assign to your CloudFront distribution.** *TileJSON responses won't work without setting this.* Example: `tiles.example.com` -* In the **Code** tab, replace the code contents with the bundled [index.mjs](https://protomaps.github.io/PMTiles/lambda_function.zip) from [PMTiles/serverless/aws](https://github.com/protomaps/PMTiles/tree/main/serverless/aws). +* In the **Code** tab, replace the code contents with the bundled [index.mjs](https://pmtiles.io/lambda_function.zip) from [PMTiles/serverless/aws](https://github.com/protomaps/PMTiles/tree/main/serverless/aws). * Choose **Deploy** to deploy the function. ### 3. Lambda role permissions diff --git a/deploy/cloudflare.md b/deploy/cloudflare.md index d821325..f30169f 100644 --- a/deploy/cloudflare.md +++ b/deploy/cloudflare.md @@ -25,7 +25,7 @@ Name your uploads to storage with the `.pmtiles` extension. Your tile requests t 2. It will ask you to deploy it first before you can edit the code, click **Deploy**. -3. **Edit Code** and paste the bundled [index.js](https://protomaps.github.io/PMTiles/index.js) from [PMTiles/serverless/cloudflare](https://github.com/protomaps/PMTiles/tree/main/serverless/cloudflare). +3. **Edit Code** and paste the bundled [index.js](https://pmtiles.io/index.js) from [PMTiles/serverless/cloudflare](https://github.com/protomaps/PMTiles/tree/main/serverless/cloudflare). 4. Leave the default **HTTP handler** option. diff --git a/pmtiles/index.md b/pmtiles/index.md index f4535ed..896dcc1 100644 --- a/pmtiles/index.md +++ b/pmtiles/index.md @@ -23,7 +23,7 @@ The current specification version of PMTiles is version 3, which you can [read o The Viewer is a browser-based tool for visualizing PMTiles on a map, inspecting metadata, and debugging individual tiles. -You can access the Viewer at [protomaps.github.io/PMTiles/](https://protomaps.github.io/PMTiles/). +You can access the Viewer at [pmtiles.io](https://pmtiles.io/). ### Serving files locally @@ -50,7 +50,7 @@ See the docs on viewing PMTiles in [Leaflet](/pmtiles/leaflet), [MapLibre GL JS] Each of the client integrations uses the [JavaScript pmtiles library](https://github.com/protomaps/PMTiles/tree/main/js). -[JavaScript Library API documentation](https://protomaps.github.io/PMTiles/typedoc/) +[JavaScript Library API documentation](https://pmtiles.io/typedoc/) ### Python diff --git a/pmtiles/openlayers.md b/pmtiles/openlayers.md index 078b6c1..327a0c7 100644 --- a/pmtiles/openlayers.md +++ b/pmtiles/openlayers.md @@ -9,9 +9,9 @@ See [the openlayers/ directory](https://github.com/protomaps/PMTiles/tree/main/o ## Quick Example -Example of vector tiles (New Zealand buildings): [Live demo](https://protomaps.github.io/PMTiles/examples/openlayers/vector.html) +Example of vector tiles (New Zealand buildings): [Live demo](https://pmtiles.io/examples/openlayers/vector.html) -Example of raster tiles (RGB Terrain): [Live demo](https://protomaps.github.io/PMTiles/examples/openlayers/raster.html) +Example of raster tiles (RGB Terrain): [Live demo](https://pmtiles.io/examples/openlayers/raster.html) ## Using with a Bundler From 5750c48c695b742b040935ab69973b37cb514957 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Tue, 18 Jun 2024 16:54:16 -0400 Subject: [PATCH 07/88] Update basemaps docs (#41) * update basemaps package version to 3.0.1 * flesh out basemap styles page * options to load basemap styles for maplibre: cdn, npm, json * prettier * fix links --- .vitepress/config.mts | 2 +- basemaps/maplibre.md | 59 ++++++++++++++++++++++++++++++++------ basemaps/styles.md | 10 ------- basemaps/themes.md | 44 ++++++++++++++++++++++++++++ components/MaplibreMap.vue | 15 +++++++--- guide/getting-started.md | 2 +- package-lock.json | 9 +++--- package.json | 2 +- 8 files changed, 113 insertions(+), 30 deletions(-) delete mode 100644 basemaps/styles.md create mode 100644 basemaps/themes.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index f90e729..ffc5fef 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -63,7 +63,7 @@ export default defineConfig({ items: [ { text: "Downloads", link: "/basemaps/downloads" }, { text: "Basemap Layers", link: "/basemaps/layers" }, - { text: "Basemap Styles", link: "/basemaps/styles" }, + { text: "Basemap Themes", link: "/basemaps/themes" }, { text: "MapLibre GL", link: "/basemaps/maplibre" }, { text: "Leaflet", link: "/basemaps/leaflet" }, { text: "OpenLayers", link: "/basemaps/openlayers" }, diff --git a/basemaps/maplibre.md b/basemaps/maplibre.md index cf5823a..f99d74b 100644 --- a/basemaps/maplibre.md +++ b/basemaps/maplibre.md @@ -5,20 +5,58 @@ outline: deep # Basemaps for MapLibre +## Assets -The `protomaps-themes-base` NPM module contains basemap layer definitions compatible with OpenStreetMap downloads from Protomaps. +To render a full basemap, you'll need not only a style and a tileset, but also MapLibre [fontstack](https://maplibre.org/maplibre-style-spec/glyphs/) and [spritesheet](https://maplibre.org/maplibre-style-spec/sprite/) assets. + +The assets referenced by the `glyphs` and `sprite` style properties can be downloaded as ZIP files at the [basemaps-assets](http://github.com/protomaps/basemaps-assets) repository, if you need to host them yourself or offline. + +### Fonts + +The `glyphs` key references a URL hosting pre-compiled fontstacks, required for displaying text labels in MapLibre. Fontstacks can be created with the [font-maker](https://github.com/maplibre/font-maker) tool. + +```js +glyphs:'https://protomaps.github.io/basemaps-assets/fonts/%7Bfontstack%7D/%7Brange%7D.pbf' +``` + +When a style layer defines a `text-font` like `Noto Sans Regular`, this will create requests for a URL like `https://protomaps.github.io/basemaps-assets/fonts/Noto%20Sans%20Regular/0-255.pbf`. + + +You can view a list of available fonts [in the GitHub repository](https://github.com/protomaps/basemaps-assets/tree/main/fonts). + +### Sprites + +The `sprite` key references a URL specific to one of [the default themes](/basemaps/themes): + +```js +sprite: "https://protomaps.github.io/basemaps-assets/sprites/v3/light" +``` + +These are required for townspots, highway shields and point of interest icons. + +## Loading styles as JSON + +Because [MapLibre styles](https://maplibre.org/maplibre-style-spec/) are JSON documents, the simplest way to define a style in your application is with static JSON. You can use the `Get style JSON` feature of [maps.protomaps.com](maps.protomaps.com) to generate static JSON for a specific theme and style package version. + +## Creating styles programatically + +For more control and less code, you can add use the [`protomaps-themes-base`](https://www.npmjs.com/package/protomaps-themes-base) NPM package as a dependency. + +### Using the npm package ```bash npm install protomaps-themes-base ``` + ```js import layers from 'protomaps-themes-base'; ``` ```js style: { - version:8, + version: 8, glyphs:'https://protomaps.github.io/basemaps-assets/fonts/%7Bfontstack%7D/%7Brange%7D.pbf', + sprite: "https://protomaps.github.io/basemaps-assets/sprites/v3/light", sources: { "protomaps": { type: "vector", @@ -33,15 +71,18 @@ style: { the default export from `protomaps-themes-base` is a function that takes 2 arguments: -* the source name of the basemap. +* the source name of the basemap, like `protomaps` in the `sources` example above. -* the theme, one of `light`, `dark`, `white`, `black`, `grayscale` or `debug`. +* the [theme](/basemaps/themes), one of `light`, `dark`, `white`, `black`, `grayscale`. -## Fonts +### Using a CDN -The fonts referenced by the `glyphs` style key can be downloaded as a ZIP at the [basemaps-assets](http://github.com/protomaps/basemaps-assets) GitHub repository. +Loading the `protomaps-themes-base` package from NPM will define the `protomaps_themes_base` global variable. -Valid font names are: `Noto Sans Regular`, `Noto Sans Medium`, `Noto Sans Italic` - -Prior to version 2.0.0-alpha.3, the Glyphs URL was `https://cdn.protomaps.com/fonts/pbf/{fontstack}/{range}.pbf`. +```html + +``` +```js +layers: protomaps_themes_base.default("protomaps","light") +```` diff --git a/basemaps/styles.md b/basemaps/styles.md deleted file mode 100644 index ba5263a..0000000 --- a/basemaps/styles.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Basemap Styles -outline: deep ---- - -# Basemap Styles - -::: warning -This section is under construction. -::: \ No newline at end of file diff --git a/basemaps/themes.md b/basemaps/themes.md new file mode 100644 index 0000000..729fad3 --- /dev/null +++ b/basemaps/themes.md @@ -0,0 +1,44 @@ +--- +title: Basemap Themes +outline: deep +--- + + + +# Basemap Themes + +These examples use the preferred [MapLibre GL JS](/basemaps/maplibre) library. + +## Default Styles + +### light + +A general-purpose basemap style. + + + +### dark + +A general-purpose basemap style. + + + +### white + +A style for data visualization. + + + +### grayscale + +A style for data visualization. + + + +### black + +A style for data visualization. + + \ No newline at end of file diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index b155efb..a76ac01 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -9,10 +9,13 @@ const { isDark } = useData(); const mapRef = ref(null); var map; -const style = () => { +const style = (passedTheme: string) => { + const theme = passedTheme || (isDark.value ? "dark" : "light"); return { version: 8, - glyphs: "https://cdn.protomaps.com/fonts/pbf/%7Bfontstack%7D/%7Brange%7D.pbf", + glyphs: + "https://protomaps.github.io/basemaps-assets/fonts/%7Bfontstack%7D/%7Brange%7D.pbf", + sprite: `https://protomaps.github.io/basemaps-assets/sprites/v3/${theme}`, sources: { protomaps: { type: "vector", @@ -25,14 +28,18 @@ const style = () => { transition: { duration: 0, }, - layers: layers("protomaps", isDark.value ? "dark" : "light"), + layers: layers("protomaps", theme), }; }; +const props = defineProps<{ + theme: string; +}>(); + onMounted(() => { map = new maplibregl.Map({ container: mapRef.value, - style: style(), + style: style(props.theme), cooperativeGestures: true, }); }); diff --git a/guide/getting-started.md b/guide/getting-started.md index e7e605d..9dca09f 100644 --- a/guide/getting-started.md +++ b/guide/getting-started.md @@ -59,5 +59,5 @@ pmtiles extract https://build.protomaps.com/{{ dateNoDashes }}.pmtiles my_area.p ## Next Steps * Upload your tiles to cloud storage: [Cloud Storage](/pmtiles/cloud-storage) -* Change the appearance or theme of the basemap: [Basemap Styles](/basemaps/styles) +* Change the appearance or theme of the basemap: [Basemap Styles](/basemaps/themes) * Bring your own datasets: [Creating PMTiles](/pmtiles/create) diff --git a/package-lock.json b/package-lock.json index 5008d61..f40507a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "maplibre-gl": "^3.4.1", "pmtiles": "^2.10.0", - "protomaps-themes-base": "^2.0.0-alpha.0", + "protomaps-themes-base": "3.0.1", "vitepress": "^1.0.0-rc.15" }, "devDependencies": { @@ -1393,9 +1393,10 @@ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, "node_modules/protomaps-themes-base": { - "version": "2.0.0-alpha.0", - "resolved": "https://registry.npmjs.org/protomaps-themes-base/-/protomaps-themes-base-2.0.0-alpha.0.tgz", - "integrity": "sha512-/Hqtwu2fVEcyrbimfo0eV6zb1QE+BeOnOBvKlrWRSWM0f745qJ7sBfqFVVoeMu7aFjDSkeW+j+g+spDhPvdGdQ==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/protomaps-themes-base/-/protomaps-themes-base-3.0.1.tgz", + "integrity": "sha512-itF0zqLYzEc/fxKdxZyc6D9GFxSnFQi53Hp+vIguuMCrHSveH9ixeRqh8Wv02woM7dRNbbbfOCeHbSWxlCdcxw==", + "license": "BSD-3-Clause" }, "node_modules/quickselect": { "version": "2.0.0", diff --git a/package.json b/package.json index 9821068..8cb3ca5 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dependencies": { "maplibre-gl": "^3.4.1", "pmtiles": "^2.10.0", - "protomaps-themes-base": "^2.0.0-alpha.0", + "protomaps-themes-base": "3.0.1", "vitepress": "^1.0.0-rc.15" }, "devDependencies": { From 6b8e62dc66b79a92cb03ad1ea7641970ee4e238a Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 19 Jun 2024 11:41:30 -0400 Subject: [PATCH 08/88] maplibre basemaps: fix link --- basemaps/maplibre.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basemaps/maplibre.md b/basemaps/maplibre.md index f99d74b..ec81083 100644 --- a/basemaps/maplibre.md +++ b/basemaps/maplibre.md @@ -36,7 +36,7 @@ These are required for townspots, highway shields and point of interest icons. ## Loading styles as JSON -Because [MapLibre styles](https://maplibre.org/maplibre-style-spec/) are JSON documents, the simplest way to define a style in your application is with static JSON. You can use the `Get style JSON` feature of [maps.protomaps.com](maps.protomaps.com) to generate static JSON for a specific theme and style package version. +Because [MapLibre styles](https://maplibre.org/maplibre-style-spec/) are JSON documents, the simplest way to define a style in your application is with static JSON. You can use the `Get style JSON` feature of [maps.protomaps.com](https://maps.protomaps.com) to generate static JSON for a specific theme and style package version. ## Creating styles programatically From 49c1e7f2fd96b38ee491991e2d40baf3e1ae59ab Mon Sep 17 00:00:00 2001 From: "Nathaniel V. KELSO" Date: Fri, 21 Jun 2024 13:07:03 -0700 Subject: [PATCH 09/88] updated docs for places layer kind_detail values (#42) --- basemaps/layers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index 151aad6..8ff04e8 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -101,7 +101,7 @@ Points from OpenStreetMap and Natural Earth, from a curated subset of place tags | Key | Values | Description | | ----------- | :-------: | -----------: | | `pmap:kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | -| `pmap:kind_detail` | `city`, `country`, `hamlet`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | +| `pmap:kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | | `capital` | string | | | `population` | int | | | `pmap:population_rank` | int | | From 51c14e43924a898e75b9a147e82a510910044597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thor=20=E9=9B=B7=E7=A5=9E=20Schaeff?= <5748289+thorwebdev@users.noreply.github.com> Date: Thu, 27 Jun 2024 04:16:41 +0800 Subject: [PATCH 10/88] Add Supabase Storage docs. (#34) --- package-lock.json | 2 +- pmtiles/cloud-storage.md | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index f40507a..a6c5ff0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,5 @@ { - "name": "docs", + "name": "protomaps-docs", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/pmtiles/cloud-storage.md b/pmtiles/cloud-storage.md index 0ebd2f0..a597094 100644 --- a/pmtiles/cloud-storage.md +++ b/pmtiles/cloud-storage.md @@ -171,6 +171,56 @@ Sample CORS Configuration: ] ``` +### Supabase Storage + +[Supabase Storage](https://supabase.com/storage) makes it simple to upload and serve files of any size, providing a robust framework for file access controls. Supabase Storage is pre-configured for CORS and HTTP Range Requests making it a plug-and-play solution for hosting PMTiles via [public buckets](https://supabase.com/docs/guides/storage/serving/downloads#public-buckets). + +#### Restricting Access + +All files uploaded in a public bucket are publicly accessible and benefit from a high CDN cache HIT ratio. However you might want to limit access to specific domains. Currently this is only possible by proxying requests through [Supabase Edge Functions](https://supabase.com/edge-functions). + +You can create a simple proxy edge functions that validates the request origin and attaches an header with your projects service role key. This allows you to serve files from private buckets while still benefitting from the built in [smart CDN](https://supabase.com/docs/guides/storage/cdn/smart-cdn). + +```ts +const ALLOWED_ORIGINS = ["http://localhost:3000/"]; +const corsHeaders = { + "Access-Control-Allow-Origin": ALLOWED_ORIGINS.join(","), + "Access-Control-Allow-Headers": + "authorization, x-client-info, apikey, content-type, range, if-match", + "Access-Control-Expose-Headers": "range, accept-ranges, etag", + "Access-Control-Max-Age": "300", +}; + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response("ok", { headers: corsHeaders }); + } + + // Validate request origin. + const origin = req.headers.get("Origin"); + console.log(origin); + if (!origin || !ALLOWED_ORIGINS.includes(origin)) { + return new Response("Not Allowed", { status: 405 }); + } + + // Construct private bucket storage URL. + const reqUrl = new URL(req.url); + const url = `${ + Deno.env.get("SUPABASE_URL") + }/storage/v1/object/authenticated${reqUrl.pathname}`; + console.log(url); + + const { method, headers } = req; + // Add auth header to access file in private bucket. + const modHeaders = new Headers(headers); + modHeaders.append( + "authorization", + `Bearer ${Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!}`, + ); + return fetch(url, { method, headers: modHeaders }); +}); +``` + ## Other Platforms ### GitHub Pages From 75254e1023e1b01a1d25eeff3812e193fa54569f Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 10 Jul 2024 12:28:24 +0200 Subject: [PATCH 11/88] aws/cloudflare: deprecate CACHE_MAX_AGE in favor of CACHE_CONTROL (#45) --- deploy/aws.md | 5 +++-- deploy/cloudflare.md | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/deploy/aws.md b/deploy/aws.md index 50556a4..3a6e74f 100644 --- a/deploy/aws.md +++ b/deploy/aws.md @@ -114,9 +114,10 @@ Configure these Lambda environment variables: * `BUCKET`: the S3 bucket name. * `PMTILES_PATH`: optional, define how a tileset name is translated into an S3 key. Default `{name}.pmtiles` * Example path setting for objects in a directory: `my_folder/{name}/file.pmtiles` -* ~~`TILE_PATH`: optional, define the URL route of the tiles API. Default `/{name}/{z}/{x}/{y}.pbf`~~ **Deprecated** +* ~~`TILE_PATH`: optional, define the URL route of the tiles API. Default `/{name}/{z}/{x}/{y}.pbf`~~ * `CORS`: optional, set the value of the `Access-Control-Allow-Origin` response header. Examples: `https://example.com`, `*`. Only supports one origin, so useful for development or staging environments only. For production use you should use CloudFront CORS configuration. -* `CACHE_MAX_AGE`: max age in the CloudFront cache, in seconds. default 86400, or 1 day. +* ~~`CACHE_MAX_AGE`: max age in the CloudFront cache, in seconds. default 86400, or 1 day.~~ +* `CACHE_CONTROL`: HTTP header value to control caching, default `public, max-age=86400` (1 day). ## Accessing your Tiles diff --git a/deploy/cloudflare.md b/deploy/cloudflare.md index f30169f..0f297cc 100644 --- a/deploy/cloudflare.md +++ b/deploy/cloudflare.md @@ -81,13 +81,15 @@ Optional environment variables can be set set in `[vars]` of `wrangler.toml` or * `PMTILES_PATH` - A string like `folder/{name}.pmtiles` specifying the path to archives in your bucket. Default `{name}.pmtiles` -* ~~`TILES_PATH` - a string like `prefix/{name}/{z}/{x}/{y}.{ext}` specifying the tile path exposed by the worker. Default `{name}/{z}/{x}/{y}.{ext}`~~ **Deprecated** +* ~~`TILES_PATH` - a string like `prefix/{name}/{z}/{x}/{y}.{ext}` specifying the tile path exposed by the worker. Default `{name}/{z}/{x}/{y}.{ext}`~~ * `PUBLIC_HOSTNAME` - Optional, override the absolute hostname in [TileJSON](https://github.com/mapbox/tilejson-spec) responses. Example `tiles.example.com` * `ALLOWED_ORIGINS` - a comma-separated list of allowed CORS origins. Default none. Examples: `https://example.com,https://localhost:3000`, `*` -* `CACHE_MAX_AGE`: max age in the Cloudflare cache, in seconds. default 86400, or 1 day. +* ~~`CACHE_MAX_AGE`: max age in the Cloudflare cache, in seconds. default 86400, or 1 day.~~ + +* `CACHE_CONTROL`: HTTP header value to control caching, default `public, max-age=86400` (1 day). ## Cost Estimate From 09398b19c2f654a554dcb0cf3bccbd7bf2bc5867 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Tue, 16 Jul 2024 12:56:40 +0200 Subject: [PATCH 12/88] Mention TileJSON in more places as this is more convenient for MapLibre. (#46) --- deploy/aws.md | 7 ++++++- deploy/cloudflare.md | 5 ++--- pmtiles/cli.md | 2 +- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/deploy/aws.md b/deploy/aws.md index 3a6e74f..6a7332d 100644 --- a/deploy/aws.md +++ b/deploy/aws.md @@ -114,7 +114,6 @@ Configure these Lambda environment variables: * `BUCKET`: the S3 bucket name. * `PMTILES_PATH`: optional, define how a tileset name is translated into an S3 key. Default `{name}.pmtiles` * Example path setting for objects in a directory: `my_folder/{name}/file.pmtiles` -* ~~`TILE_PATH`: optional, define the URL route of the tiles API. Default `/{name}/{z}/{x}/{y}.pbf`~~ * `CORS`: optional, set the value of the `Access-Control-Allow-Origin` response header. Examples: `https://example.com`, `*`. Only supports one origin, so useful for development or staging environments only. For production use you should use CloudFront CORS configuration. * ~~`CACHE_MAX_AGE`: max age in the CloudFront cache, in seconds. default 86400, or 1 day.~~ * `CACHE_CONTROL`: HTTP header value to control caching, default `public, max-age=86400` (1 day). @@ -127,6 +126,12 @@ The default Cloudfront URL for your tiles: https://SUBDOMAIN.cloudfront.net/ARCHIVE_NAME/{z}/{x}/{y}.{ext} ``` +If you're using [MapLibre](/pmtiles/maplibre), it's more convenient to fetch [TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0), which will detect the tileset `minzoom` and `maxzoom`: + +``` +https://SUBDOMAIN.cloudfront.net/ARCHIVE_NAME.json +``` + where `{ext}` is the file extension - `mvt`, `jpg`, or `png` - matching your tileset, and `SUBDOMAIN` is found at the **Distribution Domain Name** in the [CloudFront Console](https://us-east-1.console.aws.amazon.com/cloudfront/v3/). diff --git a/deploy/cloudflare.md b/deploy/cloudflare.md index 0f297cc..2d3b0f5 100644 --- a/deploy/cloudflare.md +++ b/deploy/cloudflare.md @@ -17,7 +17,7 @@ Run the following commands: 1. `rclone config` and follow the on screen questions, the endpoint should look like: `https://.r2.cloudflarestorage.com` 2. `rclone copyto :/ --progress --s3-no-check-bucket --s3-chunk-size=256M` to upload to the root of the bucket. -Name your uploads to storage with the `.pmtiles` extension. Your tile requests to the Workers URL will look like `/NAME/0/0/0.` for the archive `NAME.pmtiles`. +Name your uploads to storage with the `.pmtiles` extension. Your tile requests to the Workers URL will look like `/NAME/0/0/0.` or `/NAME.json` ([TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0)) for the archive `NAME.pmtiles`. ### 2. Create Worker with Web Console @@ -72,6 +72,7 @@ Example with `curl` for vector tiles and [TileJSON](https://github.com/mapbox/ti ```bash curl -v https://subdomain.mydomain.com/FILENAME/0/0/0.mvt +# TileJSON for MapLibre curl -v https://subdomain.mydomain.com/FILENAME.json ``` @@ -81,8 +82,6 @@ Optional environment variables can be set set in `[vars]` of `wrangler.toml` or * `PMTILES_PATH` - A string like `folder/{name}.pmtiles` specifying the path to archives in your bucket. Default `{name}.pmtiles` -* ~~`TILES_PATH` - a string like `prefix/{name}/{z}/{x}/{y}.{ext}` specifying the tile path exposed by the worker. Default `{name}/{z}/{x}/{y}.{ext}`~~ - * `PUBLIC_HOSTNAME` - Optional, override the absolute hostname in [TileJSON](https://github.com/mapbox/tilejson-spec) responses. Example `tiles.example.com` * `ALLOWED_ORIGINS` - a comma-separated list of allowed CORS origins. Default none. Examples: `https://example.com,https://localhost:3000`, `*` diff --git a/pmtiles/cli.md b/pmtiles/cli.md index b133818..503a6f3 100644 --- a/pmtiles/cli.md +++ b/pmtiles/cli.md @@ -80,7 +80,7 @@ pmtiles serve . --bucket=https://example.com pmtiles serve / --bucket=s3://BUCKET_NAME ``` -Expose Z/X/Y tile URLS, e.g. `/mymap/{z}/{x}/{y}.mvt`, for a directory or bucket of archives. **Requests for the raw file e.g. `mymap.pmtiles` will not work.** +Expose [TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0), e.g. `/mymap.json`, as well as Z/X/Y tile URLs, e.g. `/mymap/{z}/{x}/{y}.mvt`, for a directory or bucket of archives. **Requests for the raw file e.g. `mymap.pmtiles` will not work.** A Z/X/Y URL like is directly supported by web and native clients such as [MapLibre](http://maplibre.org), without needing the PMTiles client library. Using `pmtiles serve` this way also allows serving public Z/X/Y traffic from private storage buckets. From 3bb26b80176a4cbdfa83d74821987d0d5f8279fe Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Sun, 28 Jul 2024 22:38:17 +0800 Subject: [PATCH 13/88] update leaflet script URL; fix AWS TileJSON comment [#46] --- basemaps/leaflet.md | 2 +- deploy/aws.md | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/basemaps/leaflet.md b/basemaps/leaflet.md index 80962c6..97a09f8 100644 --- a/basemaps/leaflet.md +++ b/basemaps/leaflet.md @@ -9,7 +9,7 @@ The simplest way to include a map in your application via script includes tag: ```js // check for the latest version on github... - + + +# Basemap Localization + +Protomaps has several localization options for names used in text labels. + + + +## Default `name` value + +Protomaps follows OpenStreetMaps's convention where a features's primary name value is is the most common name in the local language(s). + +In practice, this is most often a single name value like: + +- `London` the locality is represented as a simple key, value pair: `name` = `London` + +However, many places have more than one common local languages and Protomaps passes thru OpenStreetMap's convention of concatenating multiple names with a `/` deliminator into a single name value, like: + +- `Switzerland` the country is represented as a complex key, value pair: `name` = `Schweiz/Suisse/Svizzera/Svizra` + +For transnational places involving many countries and languages, like `sea` features, the default name value can get quite long and unweidly! + +However, we recommended preferring localized names (see blow) for map labels, and fallback to the default name only when a localized name isn't available. + +## Localized `name:*` values + +Protomaps structures localized names using the same `name:{language_code}` formatting as OpenStreetMap. + +More than 100 countries recognize 2 or more official languages – and some like Bolivia, India, and South Africa recognize more than 10 official languages each! + +A single official language is used in most remaining countries. There are a few countries where no official language has been designated – like in the United States. + +Going back to our London example, English is the predominant (unofficial) langauge in the United Kingdom: + +- `name:en` = `London` + +Extending our London example, many other languages include [exonym and endonym](https://simple.wikipedia.org/wiki/Exonym_and_endonym#:~:text=An%20exonym%20is%20a%20name,place%20and%20language%20call%20themselves.) values in both Latin script and non-Latin scripts: + +- `name:ar` = `لندن` +- `name:de` = `London` +- `name:es` = `Londres` +- `name:fr` = `Londres` +- `name:it` = `Londra` +- `name:pt` = `Londres` +- `name:zh-Hans` = `伦敦` +- `name:zh-Hant` = `倫敦` +- _... many other localized values..._ + +Going back to our Switzerland example, each of the local (often official) languages would have a specific language name value (in this case German `de`, French `fr`, Italian `it`, and Romansh `rm`), like: + +- `name:de` = `Schweiz` +- `name:fr` = `Suisse` +- `name:it` = `Svizzera` +- `name:rm` = `Svizra` +- _... many other localized values..._ + +Extending our Switzerland example with exonym and endonym from other languages: + +- `name:ar` = `سويسرا` +- `name:en` = `Switzerland` +- `name:es` = `Switzerland` +- `name:pt` = `Suíça` +- `name:zh` = `瑞士` +- `name:zh-Hans` = `瑞士` +- `name:zh-Hant` = `瑞士` +- _... many other localized values..._ + +_NOTE: The Chinese (`zh`) examples above demonstrates how a single language can have multiple writing systems, in this case both simplified Chinese (`zh-Hans`) used in mainland China and tranditional Chinese (`zh-Hant`) used in Taiwan. The value stored in `zh` could be either of those._ + +## Script of default `name` value + +The default (or primary) `name` does not self describe the writing system "script" or character set (alphabetic, stroke-based, or otherwise) used to render the value. When combining with localized `name:*` values. This complicates preferring to "fallback" to another language in the same script family before falling back to characters using a different writing system the reader may not be able to make sense of. + +To help solve this, Protomaps characterizes the scipt used in the default `name` value by adding a `pmap:script` tag. + +Values in `pmap:script` follow the [ISO 15924](https://unicode.org/iso15924/iso15924-codes.html) standard codes for the representation of names of scripts and are summarized in the table below. + +_NOTE: Some languages can be written in more than one script, e.g., Malay can be written in Latin, Arabic, and Thai._ + +## Common languages, their codes, and scripts + +This table summarizes 26 common langauges, their ISO codes, and writing system scripts. + +| Language | Native name | `name:*` property | [ISO 639-2 code](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) | [ISO_639-1 code](https://en.wikipedia.org/wiki/ISO_639-1) | [ISO_15924 script(s)](https://unicode.org/iso15924/iso15924-codes.html) | +|--------|-----------------|-----------|-----|----|----| +| Arabic | اَلْعَرَبِيَّةُ | `name:ar` | ara | ar | `Arabic` | +| Bengali | বাংলা | `name:bn` | ben | bn | `Bengali` | +| German | Deutsch | `name:de` | deu | de | `Latin` | +| English | English | `name:en` | eng | en | `Latin` | +| Spanish | español | `name:es` | spa | es | `Latin` | +| Farsi | فارسی | `name:fa` | fas | fa | `Arabic` | +| French | français | `name:fr` | fra | fr | `Latin` | +| Greek | Νέα Ελληνικά | `name:el` | ell | el | `Greek` | +| Hebrew | עברית | `name:he` | heb | he | `Hebrew` | +| Hindi | हिन्दी | `name:hi` | hin | hi | `Devanagari` | +| Hungarian | magyar | `name:hu` | hun | hu | `Latin` | +| Indonesian | bahasa Indonesia | `name:id` | ind | id | `Latin` | +| Italian | italiano | `name:it` | ita | it | `Latin` | +| Japanese | 日本語 | `name:ja` | jpn | ja | `Han`, `Katakana`, `Hiragana` | +| Korean | 한국어 | `name:ko` | kor | ko | `Hangul` | +| Dutch | Nederlands | `name:nl` | nld | nl | `Latin` | +| Polish | Język polski | `name:pl` | pol | pl | `Latin` | +| Portuguese | português | `name:pt` | por | pt | `Latin` | +| Russian | русский язык | `name:ru` | rus | ru | `Cyrillic` | +| Swedish | svenska | `name:sv` | swe | sv | `Latin` | +| Turkish | Türkçe | `name:tr` | tur | tr | `Latin` | +| Ukrainian | Українська мова | `name:uk` | ukr | uk | `Cyrillic`, `Latin` | +| Urdu | اُردُو | `name:ur` | urd | ur | `Arabic` | +| Vietnamese | Tiếng Việt | `name:vi` | vie | vi | `Latin` | +| Chinese simplified | 中文 汉语 | `name:zh-Hans` | zho | zh | `Han` | +| Chinese traditional | 中文 漢語 | `name:zh-Hant` | zho | zh | `Han` | + +A full 2-character language code decoder ring is +[available](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes). + +_NOTE: Some langauges require codes with 3-characers or more._ + +## Common languages by country + +The following country and international organizations worldviews are supported: + +| Country | Native name | Common language | Localized `name:*` value | Recommended `name:*` pairing | +|---------|-------------|------------------|--------------------------|--------------------------| +| Argentina | Argentina | Spanish | `name:es` | `name:it`, `name:fr`, `name:en`, `name:de` | +| Bangladesh | বাংলাদেশ | Bengali | `name:bn` | _n/a_ | +| Brazil | Brasil | Portugese | `name:pt` | `name:es`, `name:it`, `name:fr`, `name:en`, `name:de` | +| China | 中国 | Chinese | `name:zh-Hans` | `name:zh`, `name:zh-Hant` | +| Egypt | مصر | Arabic | `name:ar` | `name:fr`, `name:en`, `name:de` | +| France | France | French | `name:fr` | `name:es`, `name:it`, `name:pt`, `name:en`, `name:de` | +| Germany | Deutschland | German | `name:de` | `name:en`, `name:fr`, `name:es`, `name:it` | +| Greece | Ελλάς | Greek | `name:el` | _n/a_ | +| India | भारत | Hindi and many other | `name:hi`, +++ | `name:en` | +| Indonesia | Indonesia | Indonesian | `name:id` | | +| Israel | ישראל | Hebrew | `name:he` | _n/a_ | +| Italy | Italia | Italian | `name:it` | `name:es`, `name:fr`, `name:pt`, `name:en`, `name:de` | +| Japan | 日本 | Japanese | `name:ja` | _n/a_ | +| Morocco | المغرب | Arabic | `name:ar` | `name:fr`, `name:en`, `name:de` | +| Nepal | नेपाल | Nepalese | `name:ne` | `name:en`| +| Netherlands | Nederland | Dutch | `name:nl` | `name:en`, `name:de`, `name:fr`, `name:es`, `name:it` | +| Pakistan | پاکستان | Urdu | `name:ur` | _n/a_ | +| Palestine | فلسطين | Arabic | `name:ar` | _n/a_ | +| Poland | Polska | Polish | `name:pl` | `name:de`, `name:en` | +| Portugal | Portugal | Portugese | `name:pt` | `name:es`, `name:it`, `name:fr`, `name:en`, `name:de` | +| Russia | Россия | Russian | `name:ru` | _n/a_ | +| Saudi Arabia | المملكة العربية السعودية | Arabic | `name:ar` | _n/a_ | +| South Korea | 한국 | Korean | `name:ko` | _n/a_ | +| Spain | España | Spanish | `name:es` | `name:pt`, `name:it`, `name:fr`, `name:en`, `name:de` | +| Sweden | Sverige | Swedish | `name:sv` | `name:en` | +| Taiwan | 中華民國 | Traditional Chinese | `name:zh-Hant` | `name:zh-Hans`, `name:zh`| +| Turkey | Türkiye | Turkish | `name:tr` | `name:fr`, `name:en`, `name:de` | +| Ukraine | Україна | Ukrainian | `name:uk` | `name:ru` | +| United Kingdom | United Kingdom | English, Welsh, Scottish, Irish, others | `name:en` | `name:es`, `name:fr`, `name:en`, `name:de` | +| United States | United States | English, Spanish, French, others | `name:en` | `name:es`, `name:fr`, `name:en`, `name:de` | +| Vietnam | Việt Nam | Vietnamese | `name:vi` | `name:fr`, `name:en`, `name:es`, `name:de` | + +## Positioned glyph font `pmap:pgf:name:*` values + +Protomaps adds additional names for a small set of language scripts, currently just the `Devanagari` script used for Hindi (`name:hi` and `pmap:pgf:name:hi`) and related languages. + +Rendering text in web browsers works for almost all languages and scripts and feels like magic. However, specialized map renderers like MapLibre have to reimplement text rendering and text layout which is complicated when text needs to be curved along linear map features instead of placed only horizontally or vertically. MapLibre normally assumes a one-to-one mapping between glyphs and Unicode codepoints that also exist in MapLibre font files (aka "font stacks") to accomplish the layout for a large but limited number of scripts. Plugins have been developed to extend MapLibre for **right-to-left** scripts like Arabic and Hebrew, and MapLibre has built-in support for **CJK scripts** like Chinese, Japanese, and Korean. + +To facilitate Protomap's support of additional, non-supported scripts in MapLibre (like the Devanagari script used by the Hindi language), Protomaps exports names with "positioned glphys" so MapLibre can use codepoints as indices of positioned glyphs in an additional custom "font stack". While the raw `pmap:pgf:name:*` values look like giberish when inspecting the raw values, they render correctly in MapLibre to the end user. + +See more: + +- [Traditional MapLibre Text Rendering](https://oliverwipfli.ch/about-text-rendering-in-maplibre-2023-10-17/) +- [Devanagari Positioned Glyph Fonts](https://oliverwipfli.ch/devanagari-in-the-protomaps-basemap-with-a-positioned-glyph-font-for-maplibre-2024-06-30/) + +## Styling localized name + +Labeling a map is typically localized for a specific language audience by prefering a specific name tag and falling back to similar languages (in the same writing system "script", see above), and finally falling back to the feature's default name (which could be in any script, in any language). + +### MapLibre + +#### MapLibre styling basic example + +TK TK TK + +#### MapLibre styling localized name with fallback example + +TK TK TK + +#### MapLibre styling localized name with script-based fallback example + +TK TK TK + +#### MapLibre styling positioned glyph font with script-based example + +TK TK TK + +#### MapLibre supported scripts and languages + +| Script | Languages | +| ------- | ---------| +| `Latin` | AFRIKAANS, ALBANIAN, AZERBAIJANI (also `Cyrillic`, `Arabic`), BASQUE, BOSNIAN (also `Cyrillic`), , CATALAN, CROATIAN, CZECH, DANISH, DUTCH, ENGLISH, ENGLISH (AUSTRALIAN), ENGLISH (GREAT BRITAIN), ESTONIAN, FINNISH, FILIPINO, FRENCH, FRENCH (CANADA), GALICIAN, GERMAN, HUNGARIAN, ICELANDIC, INDONESIAN, ITALIAN, KAZAKH (also `Latin`, `Arabic`, `Cyrillic`), LATVIAN, LITHUANIAN, MALAY (also `Arabic`, `Thai`), NORWEGIAN, POLISH, PORTUGUESE, PORTUGUESE (BRAZIL), PORTUGUESE (PORTUGAL), ROMANIAN, SERBIAN (also `Cyrillic`), SLOVAK (also `Cyrillic`), SLOVENIAN, SPANISH, SPANISH (LATIN AMERICA), SWAHILI, SWEDISH, TURKISH, UZBEK (also `Cyrillic`, `Arabic`), VIETNAMESE, ZULU | +| `Arabic` | ARABIC, FARSI, URDU, KAZAKH (also `Cyrillic`, `Latin`), KYRGYZ (also `Cyrillic`) | +| `Cyrillic` | BELARUSIAN, BULGARIAN (also `Latin`), KAZAKH (also `Latin`, `Arabic`), KYRGYZ (also `Arabic`), MACEDONIAN, MONGOLIAN, RUSSIAN, SERBIAN (also `Latin`), UKRAINIAN | +| `Han` | CHINESE, CHINESE (SIMPLIFIED), CHINESE (HONG KONG), CHINESE (TRADITIONAL) | +| `Amharic` | AMHARIC | +| `Armenian` | ARMENIAN | +| `Hangul` | KOREAN | +| `Hebrew` | HEBREW | +| `Japanese` | JAPANESE | +| `Georgian` | GEORGIAN | +| `Greek` | GREEK | +| `Mongolian` | MONGOLIAN (also `Cyrillic`) | + +NOTE: Right-to-left scripts and languages like Arabic and Hebrew requires a special [RTL text MapLibre plugin](https://maplibre.org/maplibre-gl-js/docs/examples/mapbox-gl-rtl-text/). + +#### MapLibre partial support + +Requires paired positioned glyph font [font stack](https://maplibre.org/maplibre-style-spec/glyphs/) paired with `pmap:pgf:name:*` values. The PGF fontstacks used by the Protomaps basemaps is available at https://github.com/protomaps/basemaps-assets/tree/main/fonts. + +| Script | Languages | +| ------- | ---------| +| `Devanagari` | GUJARATI, HINDI, MARATHI, NEPALI | + +These are primarily found in India. + +#### MapLibre no support + +| Script | Languages | +| ------- | ---------| +| `Kannada` | KANNADA | +| `Bengali` | BENGALI | +| `Burmese` | BURMESE | +| `Khmer` | KHMER | +| `Lao` | LAO | +| `Malayalam` | MALAYALAM | +| `Punjabi` | PUNJABI | +| `Sinhalese` | SINHALESE | +| `Tamil` | TAMIL | +| `Telugu` | TELUGU | +| `Thai` | THAI + +_NOTE: This is a partial listing of scripts and languages._ + +These non-supported MapLibre languages are primarily found in India and countries in south-east Asia. + +### OpenLayers + +Tk tk tk From 5ddf1a5fe93c7a6797ccd800600c69aa12fc58d6 Mon Sep 17 00:00:00 2001 From: mentalstring Date: Sat, 17 Aug 2024 01:31:25 +0100 Subject: [PATCH 15/88] Fix protomaps-leaflet package reference (#48) This fixes the URL that pointed to the wrong package name and non-existing version. --- basemaps/leaflet.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/basemaps/leaflet.md b/basemaps/leaflet.md index 97a09f8..9ed16b3 100644 --- a/basemaps/leaflet.md +++ b/basemaps/leaflet.md @@ -8,8 +8,7 @@ outline: deep The simplest way to include a map in your application via script includes tag: ```js -// check for the latest version on github... - + +``` + +## GDPR + +::: info +This is not a substitute for legal advice. +::: + +The European Union's [General Data Protection Regulation (GDPR)](https://gdpr.eu) regulates how companies store and transmit personal data. + +Using Protomaps for your web map can **eliminate third party data controllers and processors**, making it much easier for sites to comply with GDPR. + +Hosting [PMTiles](/pmtiles/) via your existing cloud storage or server is a first step - a typical map application has many other components. + +### Example Application + +Below is a complete example of a map application that avoids third-party data processors. As long as all linked assets are on your own GDPR-compliant static storage, no third party data processors or controllers are required. + +```html{4-7,15,23-24} + + + + + + + + + +
+ + + +``` + +* `maplibre-gl.js`, `maplibre-gl.css` - JavaScript and CSS for the MapLibre GL rendering library. +* `pmtiles.js` - JavaScript for decoding PMTiles archives in the browser. +* `protomaps-themes-base.js` - JavaScript for creating a MapLibre GL style for a basemap tileset. +* `mapbox-gl-rtl-text.min.js` - MapLibre plugin for supporting right-to-left languages. +* `fonts/{fontstack}/{range}.pbf` - Font glyphs for rendering labels, available at [protomaps/basemaps-assets](https://github.com/protomaps/basemaps-assets). +* `sprites/{version/{theme}` - Sprites for basemap icons, available at [protomaps/basemaps-assets](https://github.com/protomaps/basemaps-assets). \ No newline at end of file From 82612d55e22b3783306e1d7eb1813b71ba5b36ff Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 5 Sep 2024 10:36:21 +0800 Subject: [PATCH 22/88] update security docs with CSP details (#54) --- guide/security-privacy.md | 23 +++++++++++++++++++---- pmtiles/cloud-storage.md | 2 +- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/guide/security-privacy.md b/guide/security-privacy.md index e7eccfd..004f0ea 100644 --- a/guide/security-privacy.md +++ b/guide/security-privacy.md @@ -52,7 +52,19 @@ Even if your PMTiles archives or tile endpoints come from your own infrastructur src="https://unpkg.com/pmtiles@3.0.7/dist/pmtiles.js" integrity="sha384-MjejsnWXHmuz93aE35YWLh5AbS/6ceRB3Vb+ukOwqFzJRTpQ8vvbkLbNV7I0QK4f" crossorigin="anonymous" -> +/> +``` + +### Content Security Policy + +Setting a [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) via HTTP header or HTML `meta` tag can enforce all page resources, like tiles, come from the same origin. + +An example HTML CSP policy that includes [MapLibre's required CSP directives](https://maplibre.org/maplibre-gl-js/docs/#csp-directives): + +```html + ``` ## GDPR @@ -63,7 +75,7 @@ This is not a substitute for legal advice. The European Union's [General Data Protection Regulation (GDPR)](https://gdpr.eu) regulates how companies store and transmit personal data. -Using Protomaps for your web map can **eliminate third party data controllers and processors**, making it much easier for sites to comply with GDPR. +Hosting Protomaps for your web map can **eliminate third party data controllers and processors**, making it easier for sites to comply with GDPR. Hosting [PMTiles](/pmtiles/) via your existing cloud storage or server is a first step - a typical map application has many other components. @@ -71,10 +83,13 @@ Hosting [PMTiles](/pmtiles/) via your existing cloud storage or server is a firs Below is a complete example of a map application that avoids third-party data processors. As long as all linked assets are on your own GDPR-compliant static storage, no third party data processors or controllers are required. -```html{4-7,15,23-24} +```html{7-10,18,26-27,32} + @@ -95,7 +110,7 @@ Below is a complete example of a map application that avoids third-party data pr center: [11.24962,43.77078], style: { glyphs: "fonts/{fontstack}/{range}.pbf", - sprites: "sprites/v3/light", + sprite: "sprites/v3/light", version: 8, sources: { protomaps: { diff --git a/pmtiles/cloud-storage.md b/pmtiles/cloud-storage.md index 68c7db2..a493116 100644 --- a/pmtiles/cloud-storage.md +++ b/pmtiles/cloud-storage.md @@ -174,7 +174,7 @@ Sample CORS Configuration: ### Supabase Storage * [Supabase Storage](https://supabase.com/storage) is an S3-compatible storage API that supports HTTP Range Requests. -* [Private Buckets](https://supabase.com/docs/guides/storage/serving/downloads#private-buckets) ntegrate with the Supabase Auth system. +* [Private Buckets](https://supabase.com/docs/guides/storage/serving/downloads#private-buckets) integrate with the Supabase Auth system. * [Public Buckets](https://supabase.com/docs/guides/storage/serving/downloads#public-buckets) allow all CORS origins (`*`) and benefit from a CDN edge cache. #### CORS From ea38e1551a468d196f02329d87f5c1472a78d111 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 12 Sep 2024 09:44:45 +0800 Subject: [PATCH 23/88] CLI docs (#56) * update docs for CLI --- .vitepress/config.mts | 4 +- pmtiles/cli.md | 111 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 100 insertions(+), 15 deletions(-) diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 3c41261..62c5464 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -50,13 +50,13 @@ export default defineConfig({ text: "Accelerating PMTiles", collapsed: true, items: [ - { text: "Server", link: "/deploy/server" }, { text: "Overview", link: "/deploy/" }, { text: "Cost Calculator", link: "/deploy/cost" }, { text: "AWS", link: "/deploy/aws" }, - { text: "Azure", link: "/deploy/azure" }, { text: "Cloudflare", link: "/deploy/cloudflare" }, { text: "Google Cloud", link: "/deploy/google-cloud" }, + { text: "Azure", link: "/deploy/azure" }, + { text: "Server", link: "/deploy/server" }, ], }, { diff --git a/pmtiles/cli.md b/pmtiles/cli.md index 503a6f3..fc50c69 100644 --- a/pmtiles/cli.md +++ b/pmtiles/cli.md @@ -5,24 +5,61 @@ outline: deep # pmtiles CLI +## Installation + +`pmtiles` is a single binary with no external dependencies. + +The source code is on [GitHub at protomaps/go-pmtiles](https://github.com/protomaps/go-pmtiles). + +To download, see [Releases on GitHub](https://github.com/protomaps/go-pmtiles/releases) for your OS and architecture. + ## CLI Overview +### Local archives + +The CLI works with local tilesets on disk, for example: + +```sh +pmtiles show test.pmtiles +``` + ### Remote archives -Remote buckets are specified in the CLI via URLS. Commands are similar to: +However, `pmtiles` can also work with remote HTTP archives and tilesets on cloud storage, even in private buckets. +```sh +pmtiles show https://r2-public.protomaps.com/protomaps-sample-datasets/cb_2018_us_zcta510_500k.pmtiles ``` -pmtiles [COMMAND] [KEY] --bucket=[PROTOCOL]://[BUCKET_NAME] + +`pmtiles` uses the [go-cloud](https://gocloud.dev/howto/blob/) library for connecting and authenticating to cloud storage. + +Commands for S3, Azure Blob and Google Cloud Storage: + +:::info +Commands that uses URL characters like `?` and `&`, should be escaped by a backslash `\` in your shell. +::: + +```sh +pmtiles show test.pmtiles --bucket=s3://BUCKET_NAME +pmtiles show test.pmtiles --bucket=azblob://CONTAINER_NAME?storage_account=ACCOUNT +pmtiles show test.pmtiles --bucket=gs://BUCKET_NAME ``` -The bucket URL can contain query parameters like: +For S3-compatible blob storage (Minio, Cloudflare R2, etc) outside of AWS: -* `endpoint`: If not using AWS, an S3-compatible HTTPS endpoint. -* `region`: a provider-specific region string, such as `us-west-2` for AWS, and `auto` for all Cloudflare regions. +```sh +pmtiles show test.pmtiles --bucket=s3://BUCKET_NAME?endpoint=https://example.com®ion=auto +``` -Example of reading from a private Cloudflare R2 bucket: +:::info +Some S3-compatible storage servers like Minio, Ceph and SeaweedFS may require [additional URL options](https://gocloud.dev/howto/blob/#s3-compatible) like `s3ForcePathStyle=true`. +::: -Since this command uses URL characters like `?` and `&`, those **must be escaped** by a backslash `\`. +### Private buckets + +`pmtiles` uses [go-cloud's](https://gocloud.dev/howto/blob/) default authentication methods for each cloud provider. + +For example, the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables are used to sign requests to private S3-compatible buckets: ```sh export AWS_ACCESS_KEY_ID=MY_KEY @@ -30,14 +67,13 @@ export AWS_SECRET_ACCESS_KEY=MY_SECRET pmtiles show NAME.pmtiles --bucket=s3://R2_BUCKET_NAME\?endpoint=https://R2_ACCOUNT_ID.r2.cloudflarestorage.com\®ion=auto ``` -Note that S3-compatible storage servers like Minio, Ceph and SeaweedFS may require [additional URL options](https://gocloud.dev/howto/blob/#s3-compatible) like `s3ForcePathStyle=true`. - ## Commands ### show ```bash pmtiles show INPUT.pmtiles +pmtiles show INPUT.pmtiles --bucket=s3://BUCKET_NAME ``` Print an archive's header data and metadata. @@ -48,7 +84,7 @@ Print an archive's header data and metadata. pmtiles tile INPUT.pmtiles 0 0 0 ``` -Output a single tile to stdout. +Output a single tile's contents to stdout. ### verify @@ -63,6 +99,8 @@ Check that an archive is ordered correctly and has correct header information. ```bash pmtiles extract INPUT.pmtiles OUTPUT.pmtiles --bbox=MIN_LON,MIN_LAT,MAX_LON,MAX_LAT pmtiles extract INPUT.pmtiles OUTPUT.pmtiles --region=REGION.geojson +pmtiles extract https://example.com/INPUT.pmtiles OUTPUT.pmtiles --maxzoom=MAXZOOM +pmtiles extract INPUT.pmtiles OUTPUT.pmtiles --maxzoom=MAXZOOM --bucket=s3://BUCKET_NAME ``` Create a smaller archive from a larger archive. The source archive may be local or remote. The source archive must be clustered. @@ -71,26 +109,73 @@ Options: * `--maxzoom`: Extract only a subset of zoom levels. Extracting a full sub-pyramid from 0 to `maxzoom` is always an efficient operation that makes minimal I/O or network requests to the source archive. * `--minzoom`: Extract only a partial sub-pyramid. This may require many more requests than leaving the default `--minzoom=0`. Because this removes overview zoom levels, it should only be used in specific situations. +* `--region`: a [GeoJSON](https://geojson.org) Polygon, Multipolygon, Feature, or FeatureCollection. +* `--download-threads` Number of parallel requests to speed up downloads. +* `--overfetch` extra data to download to batch small requests: 0.05 is 5%. ### serve +The simplest way to consume PMTiles on the web is directly in the browser with [pmtiles.js along with a renderer-specific client](/pmtiles/maplibre). However, decoding PMTiles on the server and exposing a ZXY API works with more clients and can result in lower latency. A ZXY API is directly supported by web and native renderers such as [MapLibre](https://maplibre.org), without needing the PMTiles client library. Using `pmtiles serve` also allows serving a public API from a private storage bucket. + +:::info +When using `pmtiles serve`, requests for the raw file like `/test.pmtiles`, either whole or partial range requests, are not supported. A standard web server like Apache, Nginx or Caddy can serve those. +::: + +Serve a directory or bucket of tilesets (like TILESET.pmtiles) from local or cloud storage as a ZXY endpoint: + ```bash pmtiles serve . +# serves this directory at http://localhost:8080/TILESET/{z}/{x}/{y}.mvt +# the .pmtiles extension is added automatically +# TileJSON at http://localhost:8080/TILESET.json pmtiles serve . --bucket=https://example.com pmtiles serve / --bucket=s3://BUCKET_NAME +pmtiles serve PREFIX --bucket=s3://BUCKET_NAME ``` -Expose [TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0), e.g. `/mymap.json`, as well as Z/X/Y tile URLs, e.g. `/mymap/{z}/{x}/{y}.mvt`, for a directory or bucket of archives. **Requests for the raw file e.g. `mymap.pmtiles` will not work.** +For ZXY URLs, the extension must match the type of the tiles in the archive, for example `mvt`, `png`, `jpg`, `webp`, `avif`. + +Flags: + +* `--cors=ORIGIN` set the value of the Access-Control-Allow-Origin. * is a valid value but must be escaped in your shell. Appropriate for development use. +* `--cache-size=SIZE_MB` set the global size of the header and directory LRU cache, shared across all archives. Default is 64 MB. +* `--port=PORT` specify the HTTP port. Defaults to 8080. +* `--public-url`: Required for serving [TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0). Specify the full URL as it should appear to the browser client like `http://localhost:8080` or `https://example.com`. + +For production usage, it's recommended to run behind a CDN or reverse proxy like Caddy to handle SSL and CORS. See the guide on [Accelerating PMTiles](/deploy/). -A Z/X/Y URL like is directly supported by web and native clients such as [MapLibre](http://maplibre.org), without needing the PMTiles client library. Using `pmtiles serve` this way also allows serving public Z/X/Y traffic from private storage buckets. ### convert +Convert an [MBTiles](https://github.com/mapbox/mbtiles-spec/tree/master/1.3) archive to PMTiles. + ```bash pmtiles convert INPUT.mbtiles OUTPUT.pmtiles ``` -Convert from MBTiles. +### upload + +Upload an archive to cloud storage. + +```bash +# requires environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY credentials +pmtiles upload INPUT.pmtiles REMOTE.pmtiles --bucket=s3://BUCKET_NAME +``` + +You will need write permissions to the bucket, for example this AWS IAM policy: + +```json + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:*", + "Resource": "arn:aws:s3:::my-bucket-name/*" + } + ] + } +``` ### version From 996a46fbaa3b27237834b4a5a2b6c1124240a8a0 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 12 Sep 2024 15:40:27 +0800 Subject: [PATCH 24/88] Google cloud docs; add CLI examples for Docker image (#57) --- deploy/google-cloud.md | 29 ++++++++++++++++++++++++++--- pmtiles/cli.md | 13 +++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/deploy/google-cloud.md b/deploy/google-cloud.md index 539a29c..bb4570f 100644 --- a/deploy/google-cloud.md +++ b/deploy/google-cloud.md @@ -4,7 +4,21 @@ PMTiles can be served from a [Cloud Run container](https://console.cloud.google. ## Cloud Storage -## Service Account +The Cloud Run container is designed to work with existing Cloud Storage buckets. + +If you need to create a new Bucket: + +1. Choose **+ Create** in the [Cloud Storage console](https://console.cloud.google.com/storage). + +2. Pick a globally unique name for your bucket. + +3. For **Location Type**, choose **Region: Lowest latency within a single region** and make a region choice. + +4. Leave **Storage Class** and **Prevent Public Access** as the defaults. + +5. Uncheck **Data Protection > Soft Delete Policy**. + +6. Upload a sample PMTiles into your bucket. ## Creating a Cloud Run container @@ -12,7 +26,7 @@ PMTiles can be served from a [Cloud Run container](https://console.cloud.google. 2. Choose **Deploy one revision from an existing container image.** -3. Specify the Container image URL `protomaps/go-pmtiles:latest`. +3. Specify the Container image URL `protomaps/go-pmtiles:v1.21.0`. 4. Choose a descriptive **Service name** like `protomaps-demo`. @@ -28,7 +42,7 @@ PMTiles can be served from a [Cloud Run container](https://console.cloud.google. 2. Leave the container command blank (default entry point) - 3. Specify the arguments: `serve . --bucket=gs://BUCKET --cache-size=500 --public-url=https://example.com` replacing `BUCKET` with the name of your bucket and `https://example.com` with your custom domain. + 3. Specify the arguments: `serve . --bucket=gs://BUCKET --cache-size=500 --public-url=https://example.com` replacing `BUCKET` with the name of your bucket and `https://example.com` with your custom domain. (You may need to enter this manually in the Console for it to interpret spaces correctly.) 4. Set `Memory` to 1 GiB. @@ -36,4 +50,13 @@ PMTiles can be served from a [Cloud Run container](https://console.cloud.google. 6. Set **Maximum Number of Instances** to 1. +By default, Cloud Run projects in the same Project as the Storage bucket will create a Service Account to authenticate to the bucket. + +You should now be able to access your tileset at these URLs: + +``` +https://EXAMPLE.REGION.run.app/TILESET/0/0/0.mvt +https://EXAMPLE.REGION.run.app/TILESET.json # TileJSON,requires --public-url to be set +``` +Edge caching can be configured through [Google Cloud CDN](https://cloud.google.com/cdn?hl=en) in front of this Cloud Run URL. diff --git a/pmtiles/cli.md b/pmtiles/cli.md index fc50c69..7bfdf14 100644 --- a/pmtiles/cli.md +++ b/pmtiles/cli.md @@ -13,6 +13,8 @@ The source code is on [GitHub at protomaps/go-pmtiles](https://github.com/protom To download, see [Releases on GitHub](https://github.com/protomaps/go-pmtiles/releases) for your OS and architecture. +An official Docker Hub image is available at [protomaps/go-pmtiles](https://hub.docker.com/repository/docker/protomaps/go-pmtiles/general). + ## CLI Overview ### Local archives @@ -153,6 +155,17 @@ Convert an [MBTiles](https://github.com/mapbox/mbtiles-spec/tree/master/1.3) arc pmtiles convert INPUT.mbtiles OUTPUT.pmtiles ``` +For the Docker image: + +```sh +docker run -v $(pwd):/data --rm protomaps/go-pmtiles convert /data/INPUT.mbtiles /data/OUTPUT.pmtiles +``` + +Options: + +* `--no-deduplication`: Do not attempt to de-duplicate tile contents. Use this to speed up `convert` if you know the input has only unique tiles. +* `--tmpdir`: specify the location of the temporary directory. + ### upload Upload an archive to cloud storage. From 007d69d98693dac60d69cc69cee2b932a1ad7f6f Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 12 Sep 2024 16:54:01 +0800 Subject: [PATCH 25/88] Updates to Cloudflare walkthrough based on web console UI [#3] (#58) --- deploy/cloudflare.md | 56 ++++++++++++++++++++-------------------- pmtiles/cloud-storage.md | 5 ++++ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/deploy/cloudflare.md b/deploy/cloudflare.md index 2d3b0f5..0f089b9 100644 --- a/deploy/cloudflare.md +++ b/deploy/cloudflare.md @@ -5,67 +5,69 @@ outline: deep # Cloudflare Integration +This guide walks through setting up a ZXY API using Cloudflare R2 Storage and Workers. + +:::warning +Cloudflare R2 is known to have higher latency (500ms or higher) than other Cloud Storage products, but lower storage and no egress costs. Evaluate this as a deployment option alongside others. +::: + ## Installation ### 1. Upload to R2 Uploading via Web UI is limited to 300 MB. -Use [rclone](https://rclone.org/downloads/) to upload larger PMTiles archives to R2 (you can use the `rclone/rclone` docker image in order to avoid installation, the config is at `/etc/rclone` for mounting). -This requires a token from **R2 > Manage R2 API Tokens**. Note **Access Key ID**, the **Secret Access Key** and the **Endpoint for S3 Clients** from the API key creation screen. -Run the following commands: -1. `rclone config` and follow the on screen questions, the endpoint should look like: `https://.r2.cloudflarestorage.com` -2. `rclone copyto :/ --progress --s3-no-check-bucket --s3-chunk-size=256M` to upload to the root of the bucket. +Use [rclone](/pmtiles/cloud-storage#uploading) to upload larger PMTiles archives to R2. + +This requires a token from **R2 > Manage R2 API Tokens**. Note **Access Key ID**, the **Secret Access Key** and the **Endpoint for S3 Clients** from the API key creation screen. The S3-compatible endpoint should look like: `https://.r2.cloudflarestorage.com`. Name your uploads to storage with the `.pmtiles` extension. Your tile requests to the Workers URL will look like `/NAME/0/0/0.` or `/NAME.json` ([TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0)) for the archive `NAME.pmtiles`. -### 2. Create Worker with Web Console +### 2. Manual Option: Create Worker with Web Console -1. In the Workers left menu of the Cloudflare dashboard, choose **Create Worker**. +1. In the **Workers & Pages** menu of the Cloudflare dashboard, choose **Create > Create Worker**. -2. It will ask you to deploy it first before you can edit the code, click **Deploy**. +2. Click **Deploy** to publish the sample code. 3. **Edit Code** and paste the bundled [index.js](https://pmtiles.io/index.js) from [PMTiles/serverless/cloudflare](https://github.com/protomaps/PMTiles/tree/main/serverless/cloudflare). -4. Leave the default **HTTP handler** option. - -5. Choose **Save and Deploy** and leave the code editing window. +5. Choose **Deploy > Save and Deploy** and leave the code editing window. -6. Select the newly created worker from the Workers list. - -7. In **Settings** of your worker, choose Variables > R2 Bucket Bindings > **Add Binding**. +6. In **Settings** of your worker, choose Variables > R2 Bucket Bindings > **Add Binding**. * Create a variable named `BUCKET` and select your R2 bucket from Step 1. - * Choose **Save and Deploy**. + * Choose **Deploy**. -Your worker should now be active at its `*.workers.dev` domain. +Your worker should now be active at its `*.workers.dev` domain, visible at the **Visit** link. -Make a request for `.workers.dev//0/0/0.` to verify tiles are served. +Make a request for `/TILESET.json` to verify TileJSON is served. -### Alternative: Use Wrangler +### 2. Command-line Option: Use Wrangler 1. Clone the [PMTiles repository](https://github.com/protomaps/PMTiles) and change to the `serverless/cloudflare` directory. -2. `npm install` in `PMTiles/js` to get the dependencies of the core JS library +2. `npm install` in `PMTiles/js` to get the dependencies of the core JS library. -3. Also `npm install` in `PMTiles/serverless/cloudflare` +3. Also `npm install` in `PMTiles/serverless/cloudflare`. -4. Copy `wrangler.toml.example` to `wrangler.toml` +4. Copy `wrangler.toml.example` to `wrangler.toml`. -5. Edit `wrangler.toml`, replacing `bucket_name` with your bucket. +5. Edit `wrangler.toml`, replacing `my-bucket-development` and `my-bucket-production` with your bucket. 6. Publish the worker: `npm run deploy` +After the deploy, the `*.workers.dev` subdomain will be printed. + +Make a request for `/TILESET.json` to verify TileJSON is served. + ### 3. Create Worker Route For the cache to work, the worker must be assigned a zone on your own domain, not `workers.dev`. -1. In **Triggers** for your Worker, **Add Custom Domain** e.g. `subdomain.mydomain.com`. This will create a DNS entry in your Cloudflare site. +1. In **Settings > Triggers** for your Worker, **Add Custom Domain** e.g. `subdomain.mydomain.com`. This will create a DNS entry in your Cloudflare site. -2. In **Routes**, Assign the route `subdomain.mydomain.com/*` to your worker. This directs traffic to the above subdomain to this specific worker. - -Verify your deployment is working by checking for the `Cf-Cache-Status` header with a value of `HIT` on tile requests. This may take 2-3 attempts. +Verify your deployment is working by checking for the `Cf-Cache-Status` header with a value of `HIT` on successful (HTTP 200) requests. Example with `curl` for vector tiles and [TileJSON](https://github.com/mapbox/tilejson-spec): @@ -86,8 +88,6 @@ Optional environment variables can be set set in `[vars]` of `wrangler.toml` or * `ALLOWED_ORIGINS` - a comma-separated list of allowed CORS origins. Default none. Examples: `https://example.com,https://localhost:3000`, `*` -* ~~`CACHE_MAX_AGE`: max age in the Cloudflare cache, in seconds. default 86400, or 1 day.~~ - * `CACHE_CONTROL`: HTTP header value to control caching, default `public, max-age=86400` (1 day). ## Cost Estimate diff --git a/pmtiles/cloud-storage.md b/pmtiles/cloud-storage.md index a493116..9746a2f 100644 --- a/pmtiles/cloud-storage.md +++ b/pmtiles/cloud-storage.md @@ -21,6 +21,11 @@ PMTiles is designed to work on any S3-compatible cloud storage platform that sup rclone copyto my-filename my-configuration:my-bucket/my-folder/my-filename.pmtiles --progress --s3-chunk-size=256M ``` +RClone is also available via the [`rclone/rclone` Docker image.](https://hub.docker.com/r/rclone/rclone) + +1. `rclone config` and follow the on screen questions. In Docker, the config is located at `/etc/rclone`. +2. `rclone copyto :/ --progress --s3-no-check-bucket --s3-chunk-size=256M` to upload to the root of the bucket. + * The [aws command-line tool](https://aws.amazon.com/cli/) can be used for uploads, as well as setting CORS configuration on any S3-compatible platform. ::: info From 119d242bff8020339ab1cecc2fa6e09870caf452 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 12 Sep 2024 17:32:48 +0800 Subject: [PATCH 26/88] change AWS guide to use CloudFormation. (#59) --- deploy/aws.md | 174 ++++++++++++-------------------------------------- 1 file changed, 42 insertions(+), 132 deletions(-) diff --git a/deploy/aws.md b/deploy/aws.md index 43b17f5..f955bf6 100644 --- a/deploy/aws.md +++ b/deploy/aws.md @@ -5,150 +5,60 @@ outline: deep # AWS Integration +The AWS Deployment option is based on [CloudFormation](https://aws.amazon.com/cloudformation/), which automates the creation and deletion of all resources. You don't need to install anything; simply upload a YAML to the AWS Console. + +At the end of this walkthrough, you'll have a CDN-cached ZXY API, compatible with all major map renderers, serving tiles from a private S3 bucket. + ## Installation ### 1. Upload to S3 +The CloudFormation template is designed to work with an existing S3 bucket. + +If you need to create a new one: + * Open the [S3 Console](https://s3.console.aws.amazon.com/s3/home) and choose **Create Bucket**. -* The name must be globally unique. Choose any region, just remember the **region name.** +* Choose a globally unique bucket name, and any region, just remember the **region name.** * We'll use `protomaps-example` as a placeholder bucket name. * Leave the default *ACLs disabled* and *Block all public access* setting. - * Leave other options as the default and proceed with Create Bucket. -* **Upload** a PMTiles archive: we'll use `my_file.pmtiles` as an example: to your bucket via the Web Console or a tool like `pmtiles` or `rclone`. - -### 2. Lambda function - -* Open the **Lambda** dashboard in the **same region as your bucket.** -* Choose **Create Function**. - * Name your function `protomaps`. - * For Runtime, leave the default choice `Node.js 20.x`. - * For Architecture, choose `arm64`. - * Under Change Default Execution Role, leave the default `Create a new role with basic Lambda Permissions`. - * This will auto-generate a role name. - * Under Advanced Settings, Choose **Enable Function URL.** - * Under **Auth Type**, choose `NONE`. - * Proceed with Create Function. -* On the Configuration tab, choose **General Configuration** > **Edit**. - * set `Memory` to **512 MB**. This is required, and more cost effective than the default of 128. -* On the Configuration tab, choose **Environment Variables** > **Edit**. - * set `BUCKET` to your unique **bucket name** from Step 1. - * set `PUBLIC_HOSTNAME` to the **public custom domain name you'll assign to your CloudFront distribution.** *TileJSON responses won't work without setting this.* Example: `tiles.example.com` -* In the **Code** tab, replace the code contents with the bundled [index.mjs](https://pmtiles.io/lambda_function.zip) from [PMTiles/serverless/aws](https://github.com/protomaps/PMTiles/tree/main/serverless/aws). -* Choose **Deploy** to deploy the function. - -### 3. Lambda role permissions - -* In the **Configuration** > **Permissions** tab, follow the link under **Execution Role > Role Name** to navigate to the function's IAM role. -* On the right side, choose **Add Permissions > Create Inline Policy**. -* Choose the **JSON** tab and paste in the following: - -(Replace `protomaps-example` with your bucket created in Step 1) - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "VisualEditor0", - "Effect": "Allow", - "Action": "s3:GetObject", - "Resource": "arn:aws:s3:::protomaps-example/*" - } - ] -} -``` - -* Give this policy any name eg `protomaps-lambda` and **Create Policy**. -* Return to your Lambda function's **Configuration** and **Function URL** page. - -The Function URL that looks like `https://AAAA.lambda-url.region-name.on.aws/` is now active. - -Make a test request for the `/my_file/0/0/0.mvt` (or `.png`, `.jpg` - whatever matches your data) path in your browser. This should succeed with your tile data! - -::: warning -This Public Lambda URL should not be used directly by browsers because it lacks caching and CORS headers, which we'll configure next. -::: - -### 4. CloudFront - -* Navigate to the CloudFront Dashboard and choose **Create Distribution**. - -* Enter your Lambda Function URL under **Origin Domain**. - -* for Cache Policy, choose **CachingOptimized** which sets a default TTL of 86400 seconds. + * Leave other options as the default and proceed with **Create Bucket.** +* **Upload** a PMTiles archive. For example, save [this file](https://pmtiles.io/protomaps(vector)ODbL_firenze.pmtiles) as `example.pmtiles`. Upload to your bucket via the Web Console or [a tool like rclone.](/pmtiles/cloud-storage#uploading) -* for Viewer Protocol Policy, choose **Redirect HTTP to HTTPS**. +### 2. CloudFormation Template -* Under Response Headers Policy, choose **Create Policy**. +1. In the CloudFormation console for your AWS region, choose **Create Stack > With new resources (standard)**. - * enter a name `protomaps-cors` +2. Choose **Specify Template > Upload a template file** and upload the file [cloudformation-stack.yaml](http://pmtiles.io/cloudformation-stack.yaml). - * Enable the slider **Configure CORS**. +3. Provide a stack name of your choice. - * Choose **Customize** under Access-Control-Allow-Origin and enter full allowed origins e.g. `https://example.com` +4. for **Parameters**, specify: - * Leave other settings as the default and proceed with **Create**. + * The allowed CORS origins. By default, all sites (`*`) are authorized to make requests. Specify a comma-separated allowlist of sites e.g. `example.com,example.io`. - * Return to the CloudFront Configuration and choose `protomaps-cors` in the **Response headers policy** dropdown (you might need to refresh). + * The name of the bucket from step 1. -* Under Settings, check **HTTP/3** in addition to HTTP/2. + * The CloudFront cache TTL, which is how long tiles will be cached at the edge. Default 1 day. -* Enter a **Description** like `protomaps`. + * (Optional) the public hostname for TileJSON. If you plan to add a custom domain like `tiles.example.com`, enter that here. Otherwise leave this blank for the default `*.cloudfront.net` hostname. -* Proceed with Create Distribution. -This may take a few minutes, where the `Last modified` value will display `... Deploying`. When that's complete, you will have a working CloudFront distribution at a URL like `AAAA.cloudfront.net` that can be accessed directly from browsers. +5. Proceed with **Next > Submit**, acknowledging that it might create IAM resources. -Accessing your Distribution from a web map should verify that tiles are cached on second request. Tile headers should include: +This may take a few minutes to create the CDN distribution. When that's complete, you will have a URL like `AAAA.cloudfront.net` that can be accessed directly from browsers: ``` -x-cache: Hit from cloudfront +https://SUBDOMAIN.cloudfront.net/TILESET.json # TileJSON for MapLibre +https://SUBDOMAIN.cloudfront.net/TILESET/{z}/{x}/{y}.{ext} ``` -You may next want to assign a custom domain name to your distribution through Route 53 and Certificate Manager. - -## Configuration - -Configure these Lambda environment variables: - -* `BUCKET`: the S3 bucket name. -* `PMTILES_PATH`: optional, define how a tileset name is translated into an S3 key. Default `{name}.pmtiles` - * Example path setting for objects in a directory: `my_folder/{name}/file.pmtiles` -* `CORS`: optional, set the value of the `Access-Control-Allow-Origin` response header. Examples: `https://example.com`, `*`. Only supports one origin, so useful for development or staging environments only. For production use you should use CloudFront CORS configuration. -* ~~`CACHE_MAX_AGE`: max age in the CloudFront cache, in seconds. default 86400, or 1 day.~~ -* `CACHE_CONTROL`: HTTP header value to control caching, default `public, max-age=86400` (1 day). - -## Accessing your Tiles - -The default Cloudfront URL for your tiles: - -``` -https://SUBDOMAIN.cloudfront.net/ARCHIVE_NAME/{z}/{x}/{y}.{ext} -``` - -where `{ext}` is the file extension - `mvt`, `jpg`, or `png` - matching your tileset, and `SUBDOMAIN` is found at the **Distribution Domain Name** in the [CloudFront Console](https://us-east-1.console.aws.amazon.com/cloudfront/v3/). - -If you're using [MapLibre](/pmtiles/maplibre), it's more convenient to fetch [TileJSON](https://github.com/mapbox/tilejson-spec/tree/master/3.0.0), which will detect the tileset `minzoom` and `maxzoom`: - -``` -https://SUBDOMAIN.cloudfront.net/ARCHIVE_NAME.json -``` - -## TileJSON - -You can access a [TileJSON](https://github.com/mapbox/tilejson-spec) document for each tileset: +Accessing your Distribution from a web map should verify that tiles are cached on second request. Tile headers should include: ``` -https://PUBLIC_HOSTNAME/ARCHIVE_NAME.json +x-cache: Hit from cloudfront ``` -These endpoints will return a 404 unless the `PUBLIC_HOSTNAME` environment variable is set. - - -## Cache Invalidation - -* For AWS Cloudfront, issue prefix-based invalidations at the [Cloudfront Console](https://us-east-1.console.aws.amazon.com/cloudfront/v3/home). The first 1000 invalidations per month are free (a prefix = 1 invalidation). -* A cache purge will result in new billable events in Lambda and reads from the origin S3 object. +You may next want to assign a custom domain name to your distribution through Route 53 and Certificate Manager and update the public hostname in step 4 above. ## Monitoring @@ -158,33 +68,33 @@ These endpoints will return a 404 unless the `PUBLIC_HOSTNAME` environment varia ### Recommended Metrics * **Cache Hit Rate**: All Metrics > Cloudfront > Per-Distribution Metrics > Requests -* **Lambda Invocations**: All Metrics > Lambda > By Function Name (protomaps) > Invocations +* **Lambda Invocations**: All Metrics > Lambda > By Function Name > Invocations -* **Lambda Execution Time**: All Metrics > Lambda > By Funcion Name (protomaps) > Duration +* **Lambda Execution Time**: All Metrics > Lambda > By Funcion Name > Duration ::: info Typical execution times for a properly configured AWS install are 125 ms p50 (mean), 800 ms p99. ::: - ## Cleanup +Deleting the CloudFormation stack will delete the CloudFront distribution, Lambda function and all associated resources, but leave the bucket untouched. -* **Disable** and then **Delete** the **CloudFront Distribution**. -* Delete the **Lambda function and associated policy** -* Delete the **S3 Bucket.** -* Delete the **IAM role.** -* (Custom domain name) **Delete** the certificate in **Certificate Manager.** -* (Custom domain name) **Delete** the A and AAAA entries in **Route 53**. +## Cache Invalidation +* For AWS Cloudfront, issue prefix-based invalidations at the [Cloudfront Console](https://us-east-1.console.aws.amazon.com/cloudfront/v3/home). The first 1000 invalidations per month are free (a prefix = 1 invalidation). +* A cache purge will result in new billable events in Lambda and reads from the origin S3 object. -## Other Deployment Options +## Lambda Configuration -### Lambda@Edge +The raw Lambda function embedded in the CloudFormation template is also available at [lambda_function.zip](https://pmtiles.io/lambda_function.zip). Note that this must be uploaded as a `.zip` file containing the single file `index.js`. -Lambda@Edge's multi-region features have little benefit when fetching data from S3 in a single region, and Lambda@Edge [doesn't support](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/edge-functions-restrictions.html) environment variables or responses over 1 MB. For globally distributed caching, use CloudFront in combination with Lambda Function URLs. +Lambda environment variables: -### API Gateway +* `BUCKET`: the S3 bucket name. +* `PMTILES_PATH`: optional, define how a tileset name is translated into an S3 key. Default `{name}.pmtiles` + * Example path setting for objects in a directory: `my_folder/{name}/file.pmtiles` +* `CORS`: optional, set the value of the `Access-Control-Allow-Origin` response header. Examples: `https://example.com`, `*`. Only supports one origin, so useful for development or staging environments only. For production use you should use CloudFront CORS configuration. +* ~~`CACHE_MAX_AGE`: max age in the CloudFront cache, in seconds. default 86400, or 1 day.~~ +* `CACHE_CONTROL`: HTTP header value to control caching, default `public, max-age=86400` (1 day). -* your Lambda Proxy Integration route will need to specify a greedy capturing parameter called `proxy` e.g. `/{proxy+}` (the default). -* API Gateway responses will always be GZIP-encoded, to work around binary content detection problems. From 01db74192f82289ea090cad187e40fadd16872a2 Mon Sep 17 00:00:00 2001 From: Stillhart Date: Sat, 21 Sep 2024 04:44:28 +0200 Subject: [PATCH 27/88] Update layers.md regarding building zoom level behavior (#60) --- basemaps/layers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index 8ff04e8..20db298 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -31,7 +31,7 @@ For the previous version of the layers, see [Layers Version 2](/basemaps/layers_ ## buildings -Buildings from OpenStreetMap. +Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains invidiual osm equivalent buildings. | Key | Values | Description | | ------------ | :---------------------------: | -------------------------------------------: | From 168314547774cde5185cc9425d59fda19f177cc4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:19:16 +0800 Subject: [PATCH 28/88] Bump vite from 4.5.3 to 4.5.5 (#62) Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.3 to 4.5.5. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v4.5.5/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v4.5.5/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6c5ff0..963911e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1582,9 +1582,9 @@ } }, "node_modules/vite": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.3.tgz", - "integrity": "sha512-kQL23kMeX92v3ph7IauVkXkikdDRsYMGTVl5KY2E9OY4ONLvkHf04MDTbnfo6NKxZiDLWzVpP5oTa8hQD8U3dg==", + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.5.tgz", + "integrity": "sha512-ifW3Lb2sMdX+WU91s3R0FyQlAyLxOzCSCP37ujw0+r5POeHPwe6udWVIElKQq8gk3t7b8rkmvqC6IHBpCff4GQ==", "dependencies": { "esbuild": "^0.18.10", "postcss": "^8.4.27", From c5ee512cc13811a171e1ab08c5f775aba8b1890d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 28 Sep 2024 14:05:10 +0800 Subject: [PATCH 29/88] Bump rollup from 3.29.2 to 3.29.5 (#63) Bumps [rollup](https://github.com/rollup/rollup) from 3.29.2 to 3.29.5. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v3.29.2...v3.29.5) --- updated-dependencies: - dependency-name: rollup dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 963911e..3d03aad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1412,9 +1412,9 @@ } }, "node_modules/rollup": { - "version": "3.29.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.2.tgz", - "integrity": "sha512-CJouHoZ27v6siztc21eEQGo0kIcE5D1gVPA571ez0mMYb25LGYGKnVNXpEj5MGlepmDWGXNjDB5q7uNiPHC11A==", + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", "bin": { "rollup": "dist/bin/rollup" }, From 491067dad8f0b94d1660302223ff1af5a6371b93 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Sun, 29 Sep 2024 21:20:23 +0800 Subject: [PATCH 30/88] update layers docs for v4 (breaking changes) [#1] (#61) * update layers docs for v4 (breaking changes) [#1] * copy updates for boundaries, roads and transit [#1] --- basemaps/layers.md | 377 ++++++++++++++++++++++++++++--------- components/MaplibreMap.vue | 56 +++++- deploy/server.md | 8 + 3 files changed, 346 insertions(+), 95 deletions(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index 20db298..6ed3428 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -11,100 +11,144 @@ outline: deep OpenStreetMap layers documentation. ::: warning -This section is under construction. - -For the previous version of the layers, see [Layers Version 2](/basemaps/layers_v2) +This section is under construction for Version 4. ::: - - ## boundaries -| Key | Values | Description | -| ---------------------- | ----------------------------------------- | ----------- | -| `pmap:kind` | `country`, `region`, `county`, `locality` | | -| `pmap:kind_detail` | | | -| `pmap:min_admin_level` | | | -| `pmap:brk_a3` | | | -| `disputed` | boolean | | + + +| Key | Values | Description | +| ------------- | ----------------------------------------- | --------------------------------- | +| `kind` | `country`, `region`, `county`, `locality` | | +| `kind_detail` | integer | the minimum admin_level from OSM | +| `brk_a3` | | `brk_a3` value from Natural Earth | +| `disputed` | boolean | | + + ## buildings + Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains invidiual osm equivalent buildings. | Key | Values | Description | | ------------ | :---------------------------: | -------------------------------------------: | -| `pmap:kind` | `building`, `building_part` | Whether it is a whole building or one part. | +| `kind` | `building`, `building_part` | Whether it is a whole building or one part. | | `height` | number | May be quantized at low zoom levels. | | `min_height` | number | May be quantized at low zoom levels. | | `layer` | integer | Layer position relative to other buildings. | -## earth - -Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. - -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `earth` | | -## landcover - -Polygons from the Daylight distribution's [landcover](https://daylightmap.org/2023/10/11/landcover.html) theme, for z0-z7. - -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `barren`, `farmland`, `forest`, `glacier`, `grassland`, `scrub`, `urban_area` | | +## earth -_NOTE: It's recommended to pair with **natural** layer polygons from OpenStreetMap at mid- and high-zooms._ + -## landuse +Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. -Polygons from OpenStreetMap, from a curated subset of aeroway, amenity, area:aeroway, boundary, highway, landuse, leisure, man_made, natural, place, railway, tourism tags, for all zooms. +| Key | Values | Description | +| ------ | :-------: | -----------: | +| `kind` | `earth` | | -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `aerodrome`, `attraction`, `beach`, `cafe`, `camp_site`, `cemetery`, `college`, `commercial`, `dog_park`, `farmland`, `farmyard`, `footway`, `forest`, `garden`, `golf_course`, `grass`, `grocery`, `hospital`, `hotel`, `industrial`, `kindergarten`, `library`, `marina`, `military`, `national_park`, `nature_reserve`, `neighbourhood`, `orchard`, `other`, `park`, `pedestrian`, `pier`, `pitch`, `platform`, `playground`, `post_office`, `protected_area`, `railway`, `recreation_ground`, `residential`, `runway`, `school`, `stadium`, `supermarket`, `taxiway`, `townhall`, `university`, `zoo` | | -| `sport` | string | Which sports are played on a pitch. | -_NOTE: Additional keys are available for each original OSM tags (when available), but those will be deprecated in the next major version so should not be used for styling._ - -## natural +## landcover -Polygons from OpenStreetMap, from a curated subset of natural and landuse tags, for all zooms. + - +Polygons from the Daylight distribution's [landcover](https://daylightmap.org/2023/10/11/landcover.html) theme, for z0-z7. -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `wood`, `glacier`, `grass`, `scrub`, `sand`, `wetland`, `bare_rock`, `forest`, `meadow`, `grass` | | +| Key | Values | Description | +| ------ | :-----------------------------------------------------------------------------: | -----------: | +| `kind` | `barren`, `farmland`, `forest`, `glacier`, `grassland`, `scrub`, `urban_area` | | -_NOTE: It's recommended to pair with **landcover** layer polygons from Daylight at low-zooms._ -## physical_line +_NOTE: It's recommended to pair with **natural** layer polygons in from OpenStreetMap at mid- and high-zooms._ -::: warning -physical_line will be deprecated in v4.0. -::: +## landuse -## physical_point + + +Polygons from OpenStreetMap, from a curated subset of `aeroway`, `amenity`, `area:aeroway`, `boundary`, `highway`, `landuse`, `leisure`, `man_made`, `natural`, `place`, `railway`, `tourism` tags, for all zooms. + +| Key | Values | Description | +| ------- | :---------: | -----------------------------------: | +| `kind` | See below | | +| `sport` | string | Which sports are played on a pitch. | + + +| Kind | +| ------------------- | +| `aerodrome` | +| `attraction` | +| `bare_rock` | +| `beach` | +| `cafe` | +| `camp_site` | +| `cemetery` | +| `college` | +| `commercial` | +| `dog_park` | +| `farmland` | +| `farmyard` | +| `footway` | +| `forest` | +| `garden` | +| `glacier` | +| `golf_course` | +| `grass` | +| `grocery` | +| `hospital` | +| `hotel` | +| `industrial` | +| `kindergarten` | +| `library` | +| `marina` | +| `meadow` | +| `military` | +| `national_park` | +| `nature_reserve` | +| `neighbourhood` | +| `orchard` | +| `other` | +| `park` | +| `pedestrian` | +| `pier` | +| `pitch` | +| `platform` | +| `playground` | +| `post_office` | +| `protected_area` | +| `railway` | +| `recreation_ground` | +| `residential` | +| `runway` | +| `sand` | +| `school` | +| `scrub` | +| `stadium` | +| `supermarket` | +| `taxiway` | +| `townhall` | +| `university` | +| `wetland` | +| `wood` | +| `zoo` | -::: warning -physical_point will be deprecated in v4.0. -::: ## places Points from OpenStreetMap and Natural Earth, from a curated subset of place tags, for all zooms. + | Key | Values | Description | | ----------- | :-------: | -----------: | -| `pmap:kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | -| `pmap:kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | +| `kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | +| `kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | | `capital` | string | | | `population` | int | | -| `pmap:population_rank` | int | | +| `population_rank` | int | | | `wikidata` | string | | _NOTE: Additional keys are available for each original OSM tags (when available), but those will be deprecated in the next major version so should not be used for styling._ @@ -113,56 +157,217 @@ _NOTE: Additional keys are available for each original OSM tags (when available) Points from OpenStreetMap, from a curated subset of aeroway, amenity, attraction, boundary, craft, highway, historic, landuse, leisure, natural, railway, shop, tourism tags, for all zooms. -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `aerodrome`, `adult_gaming_centre`, `airfield`, `alpine_hut`, `amusement_ride`, `animal`, `art`, `artwork`, `atm`, `attraction`, `atv`, `baby_hatch`, `bakery`, `bbq`, `beauty`, `bed_and_breakfast`, `bench`, `bicycle_parking`, `bicycle_rental`, `bicycle_repair_station`, `boat_storage`, `bookmaker`, `books`, `bureau_de_change`, `bus_stop`, `butcher`, `cafe`, `camp_site`, `car_parts`, `car_rental`, `car_repair`, `car_sharing`, `car_wash`, `car`, `carousel`, `cemetery`, `chalet`, `charging_station`, `childcare`, `clinic`, `clothes`, `college`, `computer`, `convenience`, `customs`, `dentist`, `district`, `doctors`, `dog_park`, `drinking_water`, `emergency_phone`, `fashion`, `firepit`, `fishing`, `florist`, `forest`, `fuel`, `gambling`, `garden_centre`, `gift`, `golf_course`, `golf`, `greengrocer`, `grocery`, `guest_house`, `hairdresser`, `hanami`, `harbourmaster`, `hifi`, `hospital`, `hostel`, `hotel`, `hunting_stand`, `information`, `jewelry`, `karaoke_box`, `karaoke`, `landmark`, `library`, `life_ring`, `lottery`, `marina`, `maze`, `memorial`, `military`, `mobile_phone`, `money_transfer`, `motorcycle_parking`, `motorcycle`, `national_park`, `naval_base`, `newsagent`, `optician`, `park`, `parking`, `perfumery`, `picnic_site`, `picnic_table`, `pitch`, `playground`, `post_box`, `post_office`, `ranger_station`, `recycling`, `roller_coaster`, `sanitary_dump_station`, `school`, `scuba_diving`, `shelter`, `ship_chandler`, `shower`, `slipway`, `snowmobile`, `social_facility`, `stadium`, `stationery`, `studio`, `summer_toboggan`, `supermarket`, `swimming_area`, `taxi`, `telephone`, `tobacco`, `toilets`, `townhall`, `trail_riding_station`, `travel_agency`, `university`, `viewpoint`, `waste_basket`, `waste_disposal`, `water_point`, `water_slide`, `watering_place`, `wayside_cross`, `wilderness_hut` | | -| `cuisine` | string | | -| `religion` | string | | -| `sport` | string | | -| `iata` | string | | + + +| Key | Values | Description | +| ---------- | :---------: | -----------: | +| `kind` | See below | | +| `cuisine` | string | | +| `religion` | string | | +| `sport` | string | | +| `iata` | string | | _NOTE: The list of kind values is not comprehensive as some raw OSM tag values are passed through in the current version._ -## roads +| kind | +| ------------------------ | +| `aerodrome` | +| `adult_gaming_centre` | +| `airfield` | +| `alpine_hut` | +| `amusement_ride` | +| `animal` | +| `art` | +| `artwork` | +| `atm` | +| `attraction` | +| `atv` | +| `baby_hatch` | +| `bakery` | +| `bbq` | +| `beauty` | +| `bed_and_breakfast` | +| `bench` | +| `bicycle_parking` | +| `bicycle_rental` | +| `bicycle_repair_station` | +| `boat_storage` | +| `bookmaker` | +| `books` | +| `bureau_de_change` | +| `bus_stop` | +| `butcher` | +| `cafe` | +| `camp_site` | +| `car_parts` | +| `car_rental` | +| `car_repair` | +| `car_sharing` | +| `car_wash` | +| `car` | +| `carousel` | +| `cemetery` | +| `chalet` | +| `charging_station` | +| `childcare` | +| `clinic` | +| `clothes` | +| `college` | +| `computer` | +| `convenience` | +| `customs` | +| `dentist` | +| `district` | +| `doctors` | +| `dog_park` | +| `drinking_water` | +| `emergency_phone` | +| `fashion` | +| `firepit` | +| `fishing` | +| `florist` | +| `forest` | +| `fuel` | +| `gambling` | +| `garden_centre` | +| `gift` | +| `golf_course` | +| `golf` | +| `greengrocer` | +| `grocery` | +| `guest_house` | +| `hairdresser` | +| `hanami` | +| `harbourmaster` | +| `hifi` | +| `hospital` | +| `hostel` | +| `hotel` | +| `hunting_stand` | +| `information` | +| `jewelry` | +| `karaoke_box` | +| `karaoke` | +| `landmark` | +| `library` | +| `life_ring` | +| `lottery` | +| `marina` | +| `maze` | +| `memorial` | +| `military` | +| `mobile_phone` | +| `money_transfer` | +| `motorcycle_parking` | +| `motorcycle` | +| `national_park` | +| `naval_base` | +| `newsagent` | +| `optician` | +| `park` | +| `parking` | +| `perfumery` | +| `picnic_site` | +| `picnic_table` | +| `pitch` | +| `playground` | +| `post_box` | +| `post_office` | +| `ranger_station` | +| `recycling` | +| `roller_coaster` | +| `sanitary_dump_station` | +| `school` | +| `scuba_diving` | +| `shelter` | +| `ship_chandler` | +| `shower` | +| `slipway` | +| `snowmobile` | +| `social_facility` | +| `stadium` | +| `stationery` | +| `studio` | +| `summer_toboggan` | +| `supermarket` | +| `swimming_area` | +| `taxi` | +| `telephone` | +| `tobacco` | +| `toilets` | +| `townhall` | +| `trail_riding_station` | +| `travel_agency` | +| `university` | +| `viewpoint` | +| `waste_basket` | +| `waste_disposal` | +| `water_point` | +| `water_slide` | +| `watering_place` | +| `wayside_cross` | +| `wilderness_hut` | -Lines from OpenStreetMap, from a curated subset of highway tags, for mid- and high-zooms. -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `highway`, `major_road`, `medium_road`, `minor_road`, `path`, `other` | | -| `pmap:kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway` | | -| `ref` | string | | -| `shield_text_length` | int | | -| `network` | string | | -| `layer` | int | | -| `oneway` | string | | -| `service` | string | | -| `pmap:link` | int | | -| `pmap:level` | `-1`, `0`, `1` | | +## roads +Linear transportation features designed for movement, including highways, streets, + railways and piers from OpenStreetMap. This layer represents built infrastructure including railways. Refer to the [transit](#transit) layer for passenger services. + + + +| Key | Values | Description | +| -------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | +| `kind` | `highway`, `major_road`, `minor_road`, `path`, `other` | | +| `kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway` | | +| `ref` | string | | +| `shield_text_length` | int | | +| `network` | string | | +| `layer` | int | | +| `oneway` | string | | +| `service` | string | | +| `link` | int | | +| `level` | `-1`, `0`, `1` | | + +| kind | +| ------------ | +| `highway` | +| `major_road` | +| `minor_road` | +| `path` | +| `aerialway` | +| `ferry` | +| `pier` | +| `rail` | ## transit + + +Lines representing scheduled passenger services suitable for rendering on the map, even at lower zoom levels. For physical infrastructure, like highways and railways, see the [roads](#roads) layer. + Lines from OpenStreetMap, from a curated subset of railway, aerialway, man_made, route, and aeroway tags, for mid- and high-zooms. -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `pmap:kind` | `aerialway`, `cable_car`, `crossover`, `ferry`, `pier`, `rail`, `runway`, `siding`, `taxiway`, `yard` | | -| `pmap:kind_detail` | `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `railway`, `subway`, `tram` | | -| `ref` | string | | -| `network` | string | | -| `layer` | int | | -| `route` | string | | -| `service` | string | | +| Key | Values | Description | +| ------------- | :-----------------------------------------------------------------------------------------------------------------------: | -----------: | +| `kind` | `aerialway`, `cable_car`, `crossover`, `ferry`, `pier`, `rail`, `runway`, `siding`, `taxiway`, `yard` | | +| `kind_detail` | `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `railway`, `subway`, `tram` | | +| `ref` | string | | +| `network` | string | | +| `layer` | int | | +| `route` | string | | +| `service` | string | | ## water + + Polygons from the Natural Earth 50m `lakes` and `ocean` themes for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. | Key | Values | Description | | ------------------ | :------------------------------------------: | -----------: | -| `pmap:kind` | `water`, `lake`, `playa`, `ocean`, `other` | | -| `pmap:kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | +| `kind` | `water`, `lake`, `playa`, `ocean`, `other` | | +| `kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | | `reservoir` | boolean | | | `alkaline` | boolean | | | `intermittent` | boolean | | diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index a76ac01..b8efa61 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -9,7 +9,43 @@ const { isDark } = useData(); const mapRef = ref(null); var map; -const style = (passedTheme: string) => { +const highlightLayers = (sourceName: string, highlightName?: string) => { + if (!highlightName) return []; + return [ + { + id: "highlight_circle", + type: "circle", + filter: ["==", ["geometry-type"], "Point"], + source: sourceName, + "source-layer": highlightName, + paint: { + "circle-color": "steelblue", + }, + }, + { + id: "highlight_stroke", + type: "line", + filter: ["==", ["geometry-type"], "LineString"], + source: sourceName, + "source-layer": highlightName, + paint: { + "line-color": "steelblue", + }, + }, + { + id: "highlight_fill", + type: "fill", + filter: ["==", ["geometry-type"], "Polygon"], + source: sourceName, + "source-layer": highlightName, + paint: { + "fill-color": "steelblue", + }, + }, + ]; +}; + +const style = (passedTheme?: string, highlightLayer?: string) => { const theme = passedTheme || (isDark.value ? "dark" : "light"); return { version: 8, @@ -19,33 +55,35 @@ const style = (passedTheme: string) => { sources: { protomaps: { type: "vector", - tiles: [ - "https://api.protomaps.com/tiles/v3/%7Bz%7D/%7Bx%7D/%7By%7D.mvt?key=e6cd5633d51d8e24", - ], - maxzoom: 15, + url: "https://api.protomaps.com/tiles/v3.json?key=e6cd5633d51d8e24", }, }, transition: { duration: 0, }, - layers: layers("protomaps", theme), + layers: layers("protomaps", theme).concat( + highlightLayers("protomaps", highlightLayer), + ), }; }; const props = defineProps<{ - theme: string; + theme?: string; + highlightLayer?: string; }>(); onMounted(() => { map = new maplibregl.Map({ container: mapRef.value, - style: style(props.theme), + style: style(props.theme, props.highlightLayer), cooperativeGestures: true, + attributionControl: false, }); + map.addControl(new maplibregl.AttributionControl({ compact: false })); }); watch(isDark, () => { - map.setStyle(style()); + map.setStyle(style(props.theme, props.highlightLayer)); }); diff --git a/deploy/server.md b/deploy/server.md index b6df1d5..0af33cd 100644 --- a/deploy/server.md +++ b/deploy/server.md @@ -15,6 +15,14 @@ If you already use a server like nginx or Apache, you can run [`pmtiles serve`]( Use [Caddy Downloads](https://caddyserver.com/download?package=github.com%2Fprotomaps%2Fgo-pmtiles%2Fcaddy) to download a Caddy build with the pmtiles plugin for your OS and architecture. +```sh +caddy list-modules +... +http.handlers.pmtiles_proxy + + Non-standard modules: 1 +``` + See the Caddy docs for how to [Keep Caddy Running](https://caddyserver.com/docs/running) by installing as a system service. ## Credentials From 04a359a64c8a0eb48536a00a86ccd027d0f8a368 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Mon, 30 Sep 2024 11:12:23 +0800 Subject: [PATCH 31/88] POI icons code for showing icons in spritesheets [#1] (#64) * POI icons code for showing icons in spritesheets [#1] --- basemaps/layers.md | 290 +++++++++++++++++++++++--------------------- components/Icon.vue | 28 +++++ 2 files changed, 180 insertions(+), 138 deletions(-) create mode 100644 components/Icon.vue diff --git a/basemaps/layers.md b/basemaps/layers.md index 6ed3428..a8010b5 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -4,6 +4,20 @@ outline: deep --- # Basemap Layers @@ -169,144 +183,144 @@ Points from OpenStreetMap, from a curated subset of aeroway, amenity, attraction _NOTE: The list of kind values is not comprehensive as some raw OSM tag values are passed through in the current version._ -| kind | -| ------------------------ | -| `aerodrome` | -| `adult_gaming_centre` | -| `airfield` | -| `alpine_hut` | -| `amusement_ride` | -| `animal` | -| `art` | -| `artwork` | -| `atm` | -| `attraction` | -| `atv` | -| `baby_hatch` | -| `bakery` | -| `bbq` | -| `beauty` | -| `bed_and_breakfast` | -| `bench` | -| `bicycle_parking` | -| `bicycle_rental` | -| `bicycle_repair_station` | -| `boat_storage` | -| `bookmaker` | -| `books` | -| `bureau_de_change` | -| `bus_stop` | -| `butcher` | -| `cafe` | -| `camp_site` | -| `car_parts` | -| `car_rental` | -| `car_repair` | -| `car_sharing` | -| `car_wash` | -| `car` | -| `carousel` | -| `cemetery` | -| `chalet` | -| `charging_station` | -| `childcare` | -| `clinic` | -| `clothes` | -| `college` | -| `computer` | -| `convenience` | -| `customs` | -| `dentist` | -| `district` | -| `doctors` | -| `dog_park` | -| `drinking_water` | -| `emergency_phone` | -| `fashion` | -| `firepit` | -| `fishing` | -| `florist` | -| `forest` | -| `fuel` | -| `gambling` | -| `garden_centre` | -| `gift` | -| `golf_course` | -| `golf` | -| `greengrocer` | -| `grocery` | -| `guest_house` | -| `hairdresser` | -| `hanami` | -| `harbourmaster` | -| `hifi` | -| `hospital` | -| `hostel` | -| `hotel` | -| `hunting_stand` | -| `information` | -| `jewelry` | -| `karaoke_box` | -| `karaoke` | -| `landmark` | -| `library` | -| `life_ring` | -| `lottery` | -| `marina` | -| `maze` | -| `memorial` | -| `military` | -| `mobile_phone` | -| `money_transfer` | -| `motorcycle_parking` | -| `motorcycle` | -| `national_park` | -| `naval_base` | -| `newsagent` | -| `optician` | -| `park` | -| `parking` | -| `perfumery` | -| `picnic_site` | -| `picnic_table` | -| `pitch` | -| `playground` | -| `post_box` | -| `post_office` | -| `ranger_station` | -| `recycling` | -| `roller_coaster` | -| `sanitary_dump_station` | -| `school` | -| `scuba_diving` | -| `shelter` | -| `ship_chandler` | -| `shower` | -| `slipway` | -| `snowmobile` | -| `social_facility` | -| `stadium` | -| `stationery` | -| `studio` | -| `summer_toboggan` | -| `supermarket` | -| `swimming_area` | -| `taxi` | -| `telephone` | -| `tobacco` | -| `toilets` | -| `townhall` | -| `trail_riding_station` | -| `travel_agency` | -| `university` | -| `viewpoint` | -| `waste_basket` | -| `waste_disposal` | -| `water_point` | -| `water_slide` | -| `watering_place` | -| `wayside_cross` | -| `wilderness_hut` | +| kind | icon | +| ------------------------ | ---- | +| `aerodrome` | | +| `adult_gaming_centre` | | +| `airfield` | | +| `alpine_hut` | | +| `amusement_ride` | | +| `animal` | | +| `art` | | +| `artwork` | | +| `atm` | | +| `attraction` | | +| `atv` | | +| `baby_hatch` | | +| `bakery` | | +| `bbq` | | +| `beauty` | | +| `bed_and_breakfast` | | +| `bench` | | +| `bicycle_parking` | | +| `bicycle_rental` | | +| `bicycle_repair_station` | | +| `boat_storage` | | +| `bookmaker` | | +| `books` | | +| `bureau_de_change` | | +| `bus_stop` | | +| `butcher` | | +| `cafe` | | +| `camp_site` | | +| `car_parts` | | +| `car_rental` | | +| `car_repair` | | +| `car_sharing` | | +| `car_wash` | | +| `car` | | +| `carousel` | | +| `cemetery` | | +| `chalet` | | +| `charging_station` | | +| `childcare` | | +| `clinic` | | +| `clothes` | | +| `college` | | +| `computer` | | +| `convenience` | | +| `customs` | | +| `dentist` | | +| `district` | | +| `doctors` | | +| `dog_park` | | +| `drinking_water` | | +| `emergency_phone` | | +| `fashion` | | +| `firepit` | | +| `fishing` | | +| `florist` | | +| `forest` | | +| `fuel` | | +| `gambling` | | +| `garden_centre` | | +| `gift` | | +| `golf_course` | | +| `golf` | | +| `greengrocer` | | +| `grocery` | | +| `guest_house` | | +| `hairdresser` | | +| `hanami` | | +| `harbourmaster` | | +| `hifi` | | +| `hospital` | | +| `hostel` | | +| `hotel` | | +| `hunting_stand` | | +| `information` | | +| `jewelry` | | +| `karaoke_box` | | +| `karaoke` | | +| `landmark` | | +| `library` | | +| `life_ring` | | +| `lottery` | | +| `marina` | | +| `maze` | | +| `memorial` | | +| `military` | | +| `mobile_phone` | | +| `money_transfer` | | +| `motorcycle_parking` | | +| `motorcycle` | | +| `national_park` | | +| `naval_base` | | +| `newsagent` | | +| `optician` | | +| `park` | | +| `parking` | | +| `perfumery` | | +| `picnic_site` | | +| `picnic_table` | | +| `pitch` | | +| `playground` | | +| `post_box` | | +| `post_office` | | +| `ranger_station` | | +| `recycling` | | +| `roller_coaster` | | +| `sanitary_dump_station` | | +| `school` | | +| `scuba_diving` | | +| `shelter` | | +| `ship_chandler` | | +| `shower` | | +| `slipway` | | +| `snowmobile` | | +| `social_facility` | | +| `stadium` | | +| `stationery` | | +| `studio` | | +| `summer_toboggan` | | +| `supermarket` | | +| `swimming_area` | | +| `taxi` | | +| `telephone` | | +| `tobacco` | | +| `toilets` | | +| `townhall` | | +| `trail_riding_station` | | +| `travel_agency` | | +| `university` | | +| `viewpoint` | | +| `waste_basket` | | +| `waste_disposal` | | +| `water_point` | | +| `water_slide` | | +| `watering_place` | | +| `wayside_cross` | | +| `wilderness_hut` | | ## roads diff --git a/components/Icon.vue b/components/Icon.vue new file mode 100644 index 0000000..2841ab1 --- /dev/null +++ b/components/Icon.vue @@ -0,0 +1,28 @@ + + + + + From 9d19be7f90f4f6a0dabb5f7dbb3c21d7217cb424 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Mon, 30 Sep 2024 15:22:25 +0800 Subject: [PATCH 32/88] Improve Cloudflare web console instructions (#65) --- deploy/cloudflare.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/deploy/cloudflare.md b/deploy/cloudflare.md index 0f089b9..733d6b9 100644 --- a/deploy/cloudflare.md +++ b/deploy/cloudflare.md @@ -29,13 +29,15 @@ Name your uploads to storage with the `.pmtiles` extension. Your tile requests t 2. Click **Deploy** to publish the sample code. -3. **Edit Code** and paste the bundled [index.js](https://pmtiles.io/index.js) from [PMTiles/serverless/cloudflare](https://github.com/protomaps/PMTiles/tree/main/serverless/cloudflare). +3. **Edit Code** and replace the existing with the bundled [index.js](https://pmtiles.io/index.js) from [PMTiles/serverless/cloudflare](https://github.com/protomaps/PMTiles/tree/main/serverless/cloudflare). 5. Choose **Deploy > Save and Deploy** and leave the code editing window. -6. In **Settings** of your worker, choose Variables > R2 Bucket Bindings > **Add Binding**. +6. In **Settings** of your worker: - * Create a variable named `BUCKET` and select your R2 bucket from Step 1. + * Choose **Variables and Secrets** > **+Add**. Name a variable `ALLOWED_ORIGINS` and set it to `*` to allow all CORS origins. + + * Choose **Bindings** > **+Add** > **R2 bucket**. Name your variable `BUCKET` and select your R2 bucket from Step 1. * Choose **Deploy**. From a6f226aa34841574f85e71585405d2cdcf9b5709 Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Mon, 30 Sep 2024 11:17:12 +0200 Subject: [PATCH 33/88] Add documentation sample for dual language labels (#66) --- basemaps/localization.md | 41 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/basemaps/localization.md b/basemaps/localization.md index 6c5243a..0b43896 100644 --- a/basemaps/localization.md +++ b/basemaps/localization.md @@ -257,3 +257,44 @@ Requires paired positioned glyph font [font stack](https://maplibre.org/maplibre _NOTE: This is a partial listing of scripts and languages._ These non-supported MapLibre languages are primarily found in India and countries in south-east Asia. + +## Dual Language Labels + +With the data present in the tiles it is possible to create dual language labels, i.e., labels in two target languages. + +For example, to localize a map to Dutch (nl) and French (fr), one can use the following json snipped in a MapLibre Style: + +```json +"text-field": [ + "case", + [ + "all", + ["has", "name:nl"], + ["has", "name:fr"], + ], + // both languages are present + [ + "case", + ["==", ["get", "name:nl"], ["get", "name:fr"]], + // both languages are identical, only show one + ["get", "name:nl"], + // languages not identical, show both + [ + "format", + ["get", "name:nl"], {}, + "\n", {}, + ["get", "name:fr"], {}, + ], + ], + [ + "all", + ["!", ["has", "name:nl"]], + ["!", ["has", "name:fr"]], + ], + // none of the languages is present, use default + ["get", "name"], + // only one language is present + ["coalesce", "name:nl", "name:fr"], +] +``` + From ab0ad082ef6066b32df7749031d5bd1540205ccf Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Mon, 30 Sep 2024 17:29:29 +0800 Subject: [PATCH 34/88] add Caddy image (#67) --- deploy/server.md | 39 ++++++++++++++++++++++----------------- deploy/server_caddy.png | Bin 0 -> 148860 bytes 2 files changed, 22 insertions(+), 17 deletions(-) create mode 100644 deploy/server_caddy.png diff --git a/deploy/server.md b/deploy/server.md index 0af33cd..3792aa4 100644 --- a/deploy/server.md +++ b/deploy/server.md @@ -5,16 +5,24 @@ outline: deep # Set up a Server -PMTiles has first-class integration with [Caddy](https://caddyserver.com), a production-grade, dependency-free web server with [automatic HTTPS](https://caddyserver.com/docs/quick-starts/https). The Caddy plugin can serve buckets of archives from private S3-compatible storage, Azure, Google Cloud, public HTTP endpoints, and the filesystem. +Provisioning a dedicated server or virtual machine can be a cost-effective and low latency way method of serving PMTiles archives as Z/X/Y tile endpoints. ::: info If you already use a server like nginx or Apache, you can run [`pmtiles serve`](/pmtiles/cli) behind a reverse proxy configuration. ::: -## Installation +## Caddy + +PMTiles has first-class integration with [Caddy](https://caddyserver.com), a production-grade, dependency-free web server with [automatic HTTPS](https://caddyserver.com/docs/quick-starts/https). The Caddy plugin can serve buckets of archives from private S3-compatible storage, Azure, Google Cloud, public HTTP endpoints, and the filesystem. + +### Installation Use [Caddy Downloads](https://caddyserver.com/download?package=github.com%2Fprotomaps%2Fgo-pmtiles%2Fcaddy) to download a Caddy build with the pmtiles plugin for your OS and architecture. +![caddy plugin](server_caddy.png) + +Verify that the `pmtiles_proxy` plugin is present in your `caddy` executable: + ```sh caddy list-modules ... @@ -25,21 +33,7 @@ http.handlers.pmtiles_proxy See the Caddy docs for how to [Keep Caddy Running](https://caddyserver.com/docs/running) by installing as a system service. -## Credentials - -Credentials will be loaded from environment variables by the [gocloud](https://gocloud.dev/howto/blob/) library. -* S3 buckets can specify `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in Caddy's [unit files](https://caddyserver.com/docs/running#unit-files): - -```txt{3-4} -... -[Service] -Environment=AWS_ACCESS_KEY_ID=mykey -Environment=AWS_SECRET_ACCESS_KEY=mysecret -... -``` - - -## Caddyfile +### Caddyfile A minimal configuration for serving a bucket: @@ -69,4 +63,15 @@ localhost:2019 { For a production-ready deployment, refer to the Caddy docs on [configuring SSL](https://caddyserver.com/docs/automatic-https#hostname-requirements) and [CORS headers](https://caddyserver.com/docs/caddyfile/directives/header). +### Credentials + +Credentials will be loaded from environment variables by the [gocloud](https://gocloud.dev/howto/blob/) library. +* S3 buckets can specify `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in Caddy's [unit files](https://caddyserver.com/docs/running#unit-files): +```txt{3-4} +... +[Service] +Environment=AWS_ACCESS_KEY_ID=mykey +Environment=AWS_SECRET_ACCESS_KEY=mysecret +... +``` diff --git a/deploy/server_caddy.png b/deploy/server_caddy.png new file mode 100644 index 0000000000000000000000000000000000000000..e91a475023e56b0447550120444e1d406231d380 GIT binary patch literal 148860 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%0DdvN|w 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 Q0Dn&&Yd$J{_&WIi11dsAKmY&$ literal 0 HcmV?d00001 From 864740b009906c483687b556cf859b3380bda58a Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Mon, 30 Sep 2024 17:04:57 +0200 Subject: [PATCH 35/88] Fix typo (#68) --- basemaps/localization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basemaps/localization.md b/basemaps/localization.md index 0b43896..09bfcb5 100644 --- a/basemaps/localization.md +++ b/basemaps/localization.md @@ -262,7 +262,7 @@ These non-supported MapLibre languages are primarily found in India and countrie With the data present in the tiles it is possible to create dual language labels, i.e., labels in two target languages. -For example, to localize a map to Dutch (nl) and French (fr), one can use the following json snipped in a MapLibre Style: +For example, to localize a map to Dutch (nl) and French (fr), one can use the following json snippet in a MapLibre Style: ```json "text-field": [ From dd2a5c04a48e02d5d235e321783b104aa8e3a91f Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Tue, 1 Oct 2024 20:41:31 +0800 Subject: [PATCH 36/88] remove transit layer, reformat tables [#1] (#69) --- basemaps/layers.md | 82 ++++++++++++++++++++++++---------------------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index a8010b5..d7093b9 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -156,14 +156,15 @@ Points from OpenStreetMap and Natural Earth, from a curated subset of place tags -| Key | Values | Description | -| ----------- | :-------: | -----------: | -| `kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | -| `kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | -| `capital` | string | | -| `population` | int | | -| `population_rank` | int | | -| `wikidata` | string | | +| Key | Values | Description | +| ----------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | +| `kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | +| `kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | +| `capital` | string | | +| `population` | int | | +| `population_rank` | int | | +| `wikidata` | string | | + _NOTE: Additional keys are available for each original OSM tags (when available), but those will be deprecated in the next major version so should not be used for styling._ @@ -330,18 +331,19 @@ Linear transportation features designed for movement, including highways, street -| Key | Values | Description | -| -------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | -| `kind` | `highway`, `major_road`, `minor_road`, `path`, `other` | | -| `kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway` | | -| `ref` | string | | -| `shield_text_length` | int | | -| `network` | string | | -| `layer` | int | | -| `oneway` | string | | -| `service` | string | | -| `link` | int | | -| `level` | `-1`, `0`, `1` | | +| Key | Values | Description | +| -------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | +| `kind` | See kinds below | | +| `kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway`, `cable_car`, `pier`, `runway`, `taxiway`, `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `subway`, `tram` | | +| `ref` | string | | +| `shield_text_length` | int | | +| `network` | string | | +| `layer` | int | | +| `oneway` | string | | +| `service` | `siding`, `crossover`, `yard` | | +| `link` | int | | +| `level` | `-1`, `0`, `1` | | + | kind | | ------------ | @@ -353,6 +355,7 @@ Linear transportation features designed for movement, including highways, street | `ferry` | | `pier` | | `rail` | +| `aeroway` | ## transit @@ -360,17 +363,16 @@ Linear transportation features designed for movement, including highways, street Lines representing scheduled passenger services suitable for rendering on the map, even at lower zoom levels. For physical infrastructure, like highways and railways, see the [roads](#roads) layer. -Lines from OpenStreetMap, from a curated subset of railway, aerialway, man_made, route, and aeroway tags, for mid- and high-zooms. +This layer is currently empty. -| Key | Values | Description | -| ------------- | :-----------------------------------------------------------------------------------------------------------------------: | -----------: | -| `kind` | `aerialway`, `cable_car`, `crossover`, `ferry`, `pier`, `rail`, `runway`, `siding`, `taxiway`, `yard` | | -| `kind_detail` | `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `railway`, `subway`, `tram` | | -| `ref` | string | | -| `network` | string | | -| `layer` | int | | -| `route` | string | | -| `service` | string | | +| Key | Values | Description | +| ------------- | :------: | -----------: | +| `kind_detail` | | | +| `ref` | string | | +| `network` | string | | +| `layer` | int | | +| `route` | string | | +| `service` | string | | ## water @@ -378,13 +380,13 @@ Lines from OpenStreetMap, from a curated subset of railway, aerialway, man_made, Polygons from the Natural Earth 50m `lakes` and `ocean` themes for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. -| Key | Values | Description | -| ------------------ | :------------------------------------------: | -----------: | -| `kind` | `water`, `lake`, `playa`, `ocean`, `other` | | -| `kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | -| `reservoir` | boolean | | -| `alkaline` | boolean | | -| `intermittent` | boolean | | -| `bridge` | string | | -| `tunnel` | string | | -| `layer` | int | | +| Key | Values | Description | +| -------------- | :-----------------------------------------------------------------------------------------------: | -----------: | +| `kind` | `water`, `lake`, `playa`, `ocean`, `other` | | +| `kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | +| `reservoir` | boolean | | +| `alkaline` | boolean | | +| `intermittent` | boolean | | +| `bridge` | string | | +| `tunnel` | string | | +| `layer` | int | | From f7ec7d74e0a801735b98d7f0716da0addfa9c233 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 4 Oct 2024 01:26:26 +0800 Subject: [PATCH 37/88] basemap show v4, bump dependencies (#70) --- basemaps/downloads.md | 8 +- components/MaplibreMap.vue | 2 +- package-lock.json | 245 ++++++++++++++++++++++++------------- package.json | 6 +- 4 files changed, 166 insertions(+), 95 deletions(-) diff --git a/basemaps/downloads.md b/basemaps/downloads.md index 78dd317..145ae69 100644 --- a/basemaps/downloads.md +++ b/basemaps/downloads.md @@ -18,7 +18,7 @@ Please note that **URLs may change** and hotlinking to these downloads are disco ## Current Version -The Version 3 Protomaps basemap daily build channel is available at [maps.protomaps.com/builds](https://maps.protomaps.com/builds). +The Version 4 Protomaps basemap daily build channel is available at [maps.protomaps.com/builds](https://maps.protomaps.com/builds). This is compatible with `protomaps-themes-base` style v4.0.0 and newer. A mirror in the AWS `us-west-2` is available on [Source Cooperative (beta)](https://beta.source.coop) at the [protomaps/openstreetmap](https://beta.source.coop/repositories/protomaps/openstreetmap/) repository. This mirrors the most recent daily build only. @@ -27,9 +27,3 @@ A mirror in the AWS `us-west-2` is available on [Source Cooperative (beta)](http To download a cutout of a specific region, rather than the entire world map, see the CLI's [extract command](/pmtiles/cli#extract). If you don't need all 16 zoom levels of detail, use the `--maxzoom` option of `pmtiles extract`. Each additional zoom level roughly doubles the size of the file. - -## Previous Version - -The version 2 basemap is `protomaps-basemap-opensource-20230408.pmtiles` - -[https://r2-public.protomaps.com/protomaps-sample-datasets/protomaps-basemap-opensource-20230408.pmtiles](https://r2-public.protomaps.com/protomaps-sample-datasets/protomaps-basemap-opensource-20230408.pmtiles) diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index b8efa61..8cdfee6 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -55,7 +55,7 @@ const style = (passedTheme?: string, highlightLayer?: string) => { sources: { protomaps: { type: "vector", - url: "https://api.protomaps.com/tiles/v3.json?key=e6cd5633d51d8e24", + url: "https://api.protomaps.com/tiles/v4.json?key=e6cd5633d51d8e24", }, }, transition: { diff --git a/package-lock.json b/package-lock.json index 3d03aad..1a0f0f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,13 +1,13 @@ { - "name": "protomaps-docs", + "name": "docs", "lockfileVersion": 3, "requires": true, "packages": { "": { "dependencies": { - "maplibre-gl": "^3.4.1", - "pmtiles": "^2.10.0", - "protomaps-themes-base": "3.0.1", + "maplibre-gl": "^4.7.1", + "pmtiles": "^3.2.0", + "protomaps-themes-base": "4.0.1", "vitepress": "^1.0.0-rc.15" }, "devDependencies": { @@ -594,7 +594,8 @@ "node_modules/@mapbox/unitbezier": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", + "license": "BSD-2-Clause" }, "node_modules/@mapbox/vector-tile": { "version": "1.3.1", @@ -613,16 +614,19 @@ } }, "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "19.3.2", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.2.tgz", - "integrity": "sha512-C2JAk64XUz9v78+bpyTk1zvgjjnDsB8CCjNumyAYdWK2dvdDtILzh1AGBMdS/llX3KaHjGYxAE5wOwfdwq4Pog==", + "version": "20.3.1", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-20.3.1.tgz", + "integrity": "sha512-5ueL4UDitzVtceQ8J4kY+Px3WK+eZTsmGwha3MBKHKqiHvKrjWWwBCIl1K8BuJSc5OFh83uI8IFNoFvQxX2uUw==", + "license": "ISC", "dependencies": { "@mapbox/jsonlint-lines-primitives": "~2.0.2", "@mapbox/unitbezier": "^0.0.1", - "json-stringify-pretty-compact": "^3.0.0", + "json-stringify-pretty-compact": "^4.0.0", "minimist": "^1.2.8", + "quickselect": "^2.0.0", "rw": "^1.3.3", - "sort-object": "^3.0.3" + "sort-object": "^3.0.3", + "tinyqueue": "^3.0.0" }, "bin": { "gl-style-format": "dist/gl-style-format.mjs", @@ -630,20 +634,47 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/quickselect": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", + "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==", + "license": "ISC" + }, "node_modules/@types/geojson": { - "version": "7946.0.11", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.11.tgz", - "integrity": "sha512-L7A0AINMXQpVwxHJ4jxD6/XjZ4NDufaRlUJHjNIFKYUFBH1SvOW+neaqb0VTRSLW5suSrSu19ObFEFnfNcr+qg==" + "version": "7946.0.14", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", + "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==", + "license": "MIT" + }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/leaflet": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", + "integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } }, "node_modules/@types/mapbox__point-geometry": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", - "integrity": "sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA==" + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", + "license": "MIT" }, "node_modules/@types/mapbox__vector-tile": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.1.tgz", - "integrity": "sha512-RpwGE57xM4a/YCH6XWzfcPVRPAu/jiCll0bEGRn6a4iubN2k4xZizskQoRj8fuXyo9BpI7F3bwz3uxs0pWbGNw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", + "license": "MIT", "dependencies": { "@types/geojson": "*", "@types/mapbox__point-geometry": "*", @@ -651,14 +682,16 @@ } }, "node_modules/@types/pbf": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.3.tgz", - "integrity": "sha512-hw6bDMjvm+QTvEC+pRLpnTknQXoPu8Fnf+A+zX9HB7j/7RfYajFSbdukabo3adPwvvEHhIMafQl0R0Tpej7clQ==" + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", + "license": "MIT" }, "node_modules/@types/supercluster": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.1.tgz", - "integrity": "sha512-dNK02GO1UApgo+1KpY4jOfm3uWb2eBCMB/VMM2y8cMoF49FiqVVcOawEg19wxYcaX7SvEs370incOuFtFGrVLg==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "license": "MIT", "dependencies": { "@types/geojson": "*" } @@ -978,6 +1011,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -986,6 +1020,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -994,6 +1029,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", + "license": "MIT", "dependencies": { "bytewise-core": "^1.2.2", "typewise": "^1.0.3" @@ -1003,6 +1039,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", + "license": "MIT", "dependencies": { "typewise-core": "^1.2" } @@ -1013,9 +1050,10 @@ "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, "node_modules/earcut": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", - "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.0.tgz", + "integrity": "sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==", + "license": "ISC" }, "node_modules/esbuild": { "version": "0.18.20", @@ -1062,6 +1100,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", "dependencies": { "is-extendable": "^0.1.0" }, @@ -1096,9 +1135,10 @@ } }, "node_modules/geojson-vt": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", - "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", + "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", + "license": "ISC" }, "node_modules/get-stream": { "version": "6.0.1", @@ -1115,6 +1155,7 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1125,16 +1166,17 @@ "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" }, "node_modules/global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", + "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", + "license": "MIT", "dependencies": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" + "ini": "^4.1.3", + "kind-of": "^6.0.3", + "which": "^4.0.0" }, "engines": { - "node": ">=6" + "node": ">=16" } }, "node_modules/ieee754": { @@ -1157,14 +1199,19 @@ ] }, "node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } }, "node_modules/is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1173,6 +1220,7 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", "dependencies": { "isobject": "^3.0.1" }, @@ -1181,22 +1229,28 @@ } }, "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "license": "ISC", + "engines": { + "node": ">=16" + } }, "node_modules/isobject": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/json-stringify-pretty-compact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", - "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", + "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", + "license": "MIT" }, "node_modules/jsonc-parser": { "version": "3.2.0", @@ -1212,6 +1266,7 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1228,9 +1283,10 @@ } }, "node_modules/maplibre-gl": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.4.1.tgz", - "integrity": "sha512-RPcdaiZ52G3X+PaHQxqQ1d4I8iTIPRl4OXhPU/3o37kDf+ImLXpUVZj4p0qBCGm71n79daVzaCMG9QxfSSQbnQ==", + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.7.1.tgz", + "integrity": "sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==", + "license": "BSD-3-Clause", "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -1239,23 +1295,24 @@ "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^19.3.2", - "@types/geojson": "^7946.0.11", - "@types/mapbox__point-geometry": "^0.1.2", - "@types/mapbox__vector-tile": "^1.3.1", - "@types/pbf": "^3.0.3", - "@types/supercluster": "^7.1.1", - "earcut": "^2.2.4", - "geojson-vt": "^3.2.1", + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^3.0.0", + "geojson-vt": "^4.0.2", "gl-matrix": "^3.4.3", - "global-prefix": "^3.0.0", + "global-prefix": "^4.0.0", "kdbush": "^4.0.2", "murmurhash-js": "^1.0.0", - "pbf": "^3.2.1", + "pbf": "^3.3.0", "potpack": "^2.0.0", - "quickselect": "^2.0.0", + "quickselect": "^3.0.0", "supercluster": "^8.0.1", - "tinyqueue": "^2.0.3", + "tinyqueue": "^3.0.0", "vt-pbf": "^3.1.3" }, "engines": { @@ -1307,9 +1364,10 @@ } }, "node_modules/pbf": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.2.1.tgz", - "integrity": "sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", + "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", + "license": "BSD-3-Clause", "dependencies": { "ieee754": "^1.1.12", "resolve-protobuf-schema": "^2.1.0" @@ -1324,10 +1382,12 @@ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" }, "node_modules/pmtiles": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-2.10.0.tgz", - "integrity": "sha512-X+s6JyperpcAkKwv55MKx72ckOUB0ZjcfK4929iM0SS0MkLydEi2FSW1E8YTE1E2XaZ2TVk/MIUrbsZuXV7K2g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-3.2.0.tgz", + "integrity": "sha512-4v3Nw5xeMxaUReLZQTz3PyM4VM/Lx/Xp/rc2GGEWMl0nqAmcb+gjyi+eOTwfPu8LnB0ash36hz0dV76uYvih5A==", + "license": "BSD-3-Clause", "dependencies": { + "@types/leaflet": "^1.9.8", "fflate": "^0.8.0" } }, @@ -1393,15 +1453,16 @@ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, "node_modules/protomaps-themes-base": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/protomaps-themes-base/-/protomaps-themes-base-3.0.1.tgz", - "integrity": "sha512-itF0zqLYzEc/fxKdxZyc6D9GFxSnFQi53Hp+vIguuMCrHSveH9ixeRqh8Wv02woM7dRNbbbfOCeHbSWxlCdcxw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/protomaps-themes-base/-/protomaps-themes-base-4.0.1.tgz", + "integrity": "sha512-p7uWeV8AWVqNs4Jj3ZaO/ZMDI52fRSEbLe9dIGfGgcHjy7M1u+HsS06WsOWNnmU+i+2rOSSkF0LNZuPQ+6/w+w==", "license": "BSD-3-Clause" }, "node_modules/quickselect": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-2.0.0.tgz", - "integrity": "sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", + "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", + "license": "ISC" }, "node_modules/resolve-protobuf-schema": { "version": "2.1.0", @@ -1429,7 +1490,8 @@ "node_modules/rw": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" }, "node_modules/search-insights": { "version": "2.8.2", @@ -1441,6 +1503,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "license": "MIT", "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -1466,6 +1529,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1474,6 +1538,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -1482,6 +1547,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", + "license": "MIT", "dependencies": { "bytewise": "^1.1.0", "get-value": "^2.0.2", @@ -1506,6 +1572,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "license": "MIT", "dependencies": { "extend-shallow": "^3.0.0" }, @@ -1517,6 +1584,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "license": "MIT", "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -1529,6 +1597,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "license": "MIT", "dependencies": { "is-plain-object": "^2.0.4" }, @@ -1550,14 +1619,16 @@ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tinyqueue": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", - "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", + "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", + "license": "ISC" }, "node_modules/typewise": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", + "license": "MIT", "dependencies": { "typewise-core": "^1.2.0" } @@ -1565,12 +1636,14 @@ "node_modules/typewise-core": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", - "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" + "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==", + "license": "MIT" }, "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", + "license": "MIT", "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -1697,14 +1770,18 @@ } }, "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "isexe": "^3.1.1" }, "bin": { - "which": "bin/which" + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" } } } diff --git a/package.json b/package.json index 8cb3ca5..9fce5dd 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "prettier-check": "prettier --check components" }, "dependencies": { - "maplibre-gl": "^3.4.1", - "pmtiles": "^2.10.0", - "protomaps-themes-base": "3.0.1", + "maplibre-gl": "^4.7.1", + "pmtiles": "^3.2.0", + "protomaps-themes-base": "4.0.1", "vitepress": "^1.0.0-rc.15" }, "devDependencies": { From 5e4ff3cb86653a4c173ccc0491c5cb8deefd3856 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 4 Oct 2024 13:42:50 +0800 Subject: [PATCH 38/88] Copy on basemap layers page; mouseover interactive features; theming (#71) * Copy on basemap layers page; mouseover interactive features; theming * prettier --- basemaps/layers.md | 49 ++++++++++++++--------- components/MaplibreMap.vue | 81 +++++++++++++++++++++++++++++++++++++- 2 files changed, 109 insertions(+), 21 deletions(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index d7093b9..f944e03 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -6,31 +6,44 @@ outline: deep import MaplibreMap from '../components/MaplibreMap.vue' import Icon from '../components/Icon.vue' - const sprites = Promise.all([ - fetch("https://github.com/service/https://protomaps.github.io/basemaps-assets/sprites/v3/light@2x.json").then(resp => resp.json()), + const fetchImage = async (url) => { new Promise((resolve,reject) => { if (typeof window !== 'undefined') { const img = new Image(); img.onload = () => { resolve(img); } - img.src = "https://protomaps.github.io/basemaps-assets/sprites/v3/light@2x.png" + img.src = url; } else { resolve(null); } }) + } + + const sprites = Promise.all([ + fetch("https://github.com/service/https://protomaps.github.io/basemaps-assets/sprites/v3/light@2x.json").then(resp => resp.json()), + fetchImage("https://protomaps.github.io/basemaps-assets/sprites/v3/light@2x.png"), + fetch("https://github.com/service/https://protomaps.github.io/basemaps-assets/sprites/v3/dark@2x.json").then(resp => resp.json()), + fetchImage("https://protomaps.github.io/basemaps-assets/sprites/v3/dark@2x.png") ]); # Basemap Layers -OpenStreetMap layers documentation. +The Protomaps basemap is built on [OpenStreetMap](https://openstreetmap.org) and [Natural Earth](https://naturalearthdata.com) data. It does not include all data and tags from OSM; instead it is designed to strike a balance between tile size and completeness, for use as a general-purpose context map. -::: warning -This section is under construction for Version 4. -::: +The organization of layers and tags is derived from the [open source Tilezen project](https://tilezen.readthedocs.io/en/latest/layers/). The scope of contents and choice of data inclusion at certain zoom levels is intended to mirror the reference implementations of Tilezen styles such as [Bubble Wrap](https://tangrams.github.io/bubble-wrap/). + +The current version is **Version 4**. + +## Common Tags + +| Key | Values | Description | +| ------------------------------ | ------- | ------------------------------------------ | +| `name`, `name:*`, `pgf:name:*` | string | see [Localization](/basemaps/localization) | +| `sort_rank` | integer | Importance ranking used for rendering | ## boundaries - + | Key | Values | Description | | ------------- | ----------------------------------------- | --------------------------------- | @@ -43,9 +56,9 @@ This section is under construction for Version 4. ## buildings - + -Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains invidiual osm equivalent buildings. +Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains invidiual OSM equivalent buildings. | Key | Values | Description | | ------------ | :---------------------------: | -------------------------------------------: | @@ -57,7 +70,7 @@ Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ## earth - + Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. @@ -68,7 +81,7 @@ Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preproce ## landcover - + Polygons from the Daylight distribution's [landcover](https://daylightmap.org/2023/10/11/landcover.html) theme, for z0-z7. @@ -81,7 +94,7 @@ _NOTE: It's recommended to pair with **natural** layer polygons in from OpenStre ## landuse - + Polygons from OpenStreetMap, from a curated subset of `aeroway`, `amenity`, `area:aeroway`, `boundary`, `highway`, `landuse`, `leisure`, `man_made`, `natural`, `place`, `railway`, `tourism` tags, for all zooms. @@ -154,7 +167,7 @@ Polygons from OpenStreetMap, from a curated subset of `aeroway`, `amenity`, `are Points from OpenStreetMap and Natural Earth, from a curated subset of place tags, for all zooms. - + | Key | Values | Description | | ----------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | @@ -172,7 +185,7 @@ _NOTE: Additional keys are available for each original OSM tags (when available) Points from OpenStreetMap, from a curated subset of aeroway, amenity, attraction, boundary, craft, highway, historic, landuse, leisure, natural, railway, shop, tourism tags, for all zooms. - + | Key | Values | Description | | ---------- | :---------: | -----------: | @@ -329,7 +342,7 @@ _NOTE: The list of kind values is not comprehensive as some raw OSM tag values a Linear transportation features designed for movement, including highways, streets, railways and piers from OpenStreetMap. This layer represents built infrastructure including railways. Refer to the [transit](#transit) layer for passenger services. - + | Key | Values | Description | | -------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | @@ -359,8 +372,6 @@ Linear transportation features designed for movement, including highways, street ## transit - - Lines representing scheduled passenger services suitable for rendering on the map, even at lower zoom levels. For physical infrastructure, like highways and railways, see the [roads](#roads) layer. This layer is currently empty. @@ -376,7 +387,7 @@ This layer is currently empty. ## water - + Polygons from the Natural Earth 50m `lakes` and `ocean` themes for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index 8cdfee6..1fd68af 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -9,6 +9,21 @@ const { isDark } = useData(); const mapRef = ref(null); var map; +const tableFromProps = (props: unknown) => { + let tableHTML = ""; + + Object.entries(props).forEach(([key, value]) => { + tableHTML += ` + + + + `; + }); + + tableHTML += "
${key}${typeof value === "boolean" ? JSON.stringify(value) : value}
"; + return tableHTML; +}; + const highlightLayers = (sourceName: string, highlightName?: string) => { if (!highlightName) return []; return [ @@ -20,6 +35,7 @@ const highlightLayers = (sourceName: string, highlightName?: string) => { "source-layer": highlightName, paint: { "circle-color": "steelblue", + "circle-opacity": 0.7, }, }, { @@ -30,6 +46,7 @@ const highlightLayers = (sourceName: string, highlightName?: string) => { "source-layer": highlightName, paint: { "line-color": "steelblue", + "line-opacity": 0.7, }, }, { @@ -40,13 +57,22 @@ const highlightLayers = (sourceName: string, highlightName?: string) => { "source-layer": highlightName, paint: { "fill-color": "steelblue", + "fill-opacity": 0.7, }, }, ]; }; const style = (passedTheme?: string, highlightLayer?: string) => { - const theme = passedTheme || (isDark.value ? "dark" : "light"); + const theme = + passedTheme || + (isDark.value + ? highlightLayer + ? "black" + : "dark" + : highlightLayer + ? "white" + : "light"); return { version: 8, glyphs: @@ -70,6 +96,10 @@ const style = (passedTheme?: string, highlightLayer?: string) => { const props = defineProps<{ theme?: string; highlightLayer?: string; + center?: number; + zoom?: number; + lat?: number; + lng?: number; }>(); onMounted(() => { @@ -78,8 +108,35 @@ onMounted(() => { style: style(props.theme, props.highlightLayer), cooperativeGestures: true, attributionControl: false, + center: props.lng && props.lat ? [props.lng, props.lat] : [0, 0], + zoom: props.zoom || 0, }); map.addControl(new maplibregl.AttributionControl({ compact: false })); + + const popup = new maplibregl.Popup({ + className: "docs-popup", + closeButton: false, + closeOnClick: false, + }); + + map.on( + "mouseenter", + ["highlight_circle", "highlight_stroke", "highlight_fill"], + (e) => { + map.getCanvas().style.cursor = "pointer"; + const properties = e.features[0].properties; + popup.setLngLat(e.lngLat).setHTML(tableFromProps(properties)).addTo(map); + }, + ); + + map.on( + "mouseleave", + ["highlight_circle", "highlight_stroke", "highlight_fill"], + () => { + map.getCanvas().style.cursor = ""; + popup.remove(); + }, + ); }); watch(isDark, () => { @@ -95,9 +152,29 @@ watch(isDark, () => { @import "https://github.com/maplibre-gl/dist/maplibre-gl.css"; - From ee7e9017b9f8042b2ea8b701f96c45d97a598b3d Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 4 Oct 2024 15:15:20 +0800 Subject: [PATCH 39/88] Layers updates 2 (#72) * remove openlayers basemaps placeholder - not on near-term roadmap * add placeholder empty icons; fix road common tags --- .vitepress/config.mts | 1 - basemaps/layers.md | 434 +++++++++++++++++++++-------------------- basemaps/openlayers.md | 10 - components/Icon.vue | 16 +- 4 files changed, 231 insertions(+), 230 deletions(-) delete mode 100644 basemaps/openlayers.md diff --git a/.vitepress/config.mts b/.vitepress/config.mts index 62c5464..3bfa568 100644 --- a/.vitepress/config.mts +++ b/.vitepress/config.mts @@ -69,7 +69,6 @@ export default defineConfig({ { text: "Basemap Localization", link: "/basemaps/localization" }, { text: "MapLibre GL", link: "/basemaps/maplibre" }, { text: "Leaflet", link: "/basemaps/leaflet" }, - { text: "OpenLayers", link: "/basemaps/openlayers" }, ], }, ], diff --git a/basemaps/layers.md b/basemaps/layers.md index f944e03..9f4d54a 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -2,6 +2,7 @@ title: Basemap Layers outline: deep --- + @@ -40,6 +43,7 @@ The current version is **Version 4**. | ------------------------------ | ------- | ------------------------------------------ | | `name`, `name:*`, `pgf:name:*` | string | see [Localization](/basemaps/localization) | | `sort_rank` | integer | Importance ranking used for rendering | +| `min_zoom` | integer | Suggested zoom level to limit display | ## boundaries @@ -52,21 +56,18 @@ The current version is **Version 4**. | `brk_a3` | | `brk_a3` value from Natural Earth | | `disputed` | boolean | | - - ## buildings Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains invidiual OSM equivalent buildings. -| Key | Values | Description | -| ------------ | :---------------------------: | -------------------------------------------: | -| `kind` | `building`, `building_part` | Whether it is a whole building or one part. | -| `height` | number | May be quantized at low zoom levels. | -| `min_height` | number | May be quantized at low zoom levels. | -| `layer` | integer | Layer position relative to other buildings. | - +| Key | Values | Description | +| ------------ | :-------------------------: | ------------------------------------------: | +| `kind` | `building`, `building_part` | Whether it is a whole building or one part. | +| `height` | number | May be quantized at low zoom levels. | +| `min_height` | number | May be quantized at low zoom levels. | +| `layer` | integer | Layer position relative to other buildings. | ## earth @@ -74,10 +75,9 @@ Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. -| Key | Values | Description | -| ------ | :-------: | -----------: | -| `kind` | `earth` | | - +| Key | Values | Description | +| ------ | :-----: | ----------: | +| `kind` | `earth` | | ## landcover @@ -85,10 +85,9 @@ Polygons from the Natural Earth 50m `land` theme for z0-z4, 10m for z5, preproce Polygons from the Daylight distribution's [landcover](https://daylightmap.org/2023/10/11/landcover.html) theme, for z0-z7. -| Key | Values | Description | -| ------ | :-----------------------------------------------------------------------------: | -----------: | -| `kind` | `barren`, `farmland`, `forest`, `glacier`, `grassland`, `scrub`, `urban_area` | | - +| Key | Values | Description | +| ------ | :---------------------------------------------------------------------------: | ----------: | +| `kind` | `barren`, `farmland`, `forest`, `glacier`, `grassland`, `scrub`, `urban_area` | | _NOTE: It's recommended to pair with **natural** layer polygons in from OpenStreetMap at mid- and high-zooms._ @@ -98,11 +97,12 @@ _NOTE: It's recommended to pair with **natural** layer polygons in from OpenStre Polygons from OpenStreetMap, from a curated subset of `aeroway`, `amenity`, `area:aeroway`, `boundary`, `highway`, `landuse`, `leisure`, `man_made`, `natural`, `place`, `railway`, `tourism` tags, for all zooms. -| Key | Values | Description | -| ------- | :---------: | -----------------------------------: | -| `kind` | See below | | -| `sport` | string | Which sports are played on a pitch. | +| Key | Values | Description | +| ------- | :-------: | ----------------------------------: | +| `kind` | See below | | +| `sport` | string | Which sports are played on a pitch. | +### Kinds | Kind | | ------------------- | @@ -162,24 +162,20 @@ Polygons from OpenStreetMap, from a curated subset of `aeroway`, `amenity`, `are | `wood` | | `zoo` | - ## places Points from OpenStreetMap and Natural Earth, from a curated subset of place tags, for all zooms. -| Key | Values | Description | -| ----------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | -| `kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | -| `kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | -| `capital` | string | | -| `population` | int | | -| `population_rank` | int | | -| `wikidata` | string | | - - -_NOTE: Additional keys are available for each original OSM tags (when available), but those will be deprecated in the next major version so should not be used for styling._ +| Key | Values | Description | +| ----------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | ----------: | +| `kind` | `country`, `region`, `locality`, `macrohood`, `neighbourhood` | | +| `kind_detail` | `allotments`, `city`, `country`, `farm`, `hamlet`, `hamlet`, `isolated_dwelling`, `locality`, `neighbourhood`, `province`, `quarter`, `scientific_station`, `state`, `town`, `village` | | +| `capital` | string | | +| `population` | int | | +| `population_rank` | int | | +| `wikidata` | string | | ## pois @@ -187,176 +183,184 @@ Points from OpenStreetMap, from a curated subset of aeroway, amenity, attraction -| Key | Values | Description | -| ---------- | :---------: | -----------: | -| `kind` | See below | | -| `cuisine` | string | | -| `religion` | string | | -| `sport` | string | | -| `iata` | string | | +| Key | Values | Description | +| ---------- | :-------: | ----------: | +| `kind` | See below | | +| `cuisine` | string | | +| `religion` | string | | +| `sport` | string | | +| `iata` | string | | _NOTE: The list of kind values is not comprehensive as some raw OSM tag values are passed through in the current version._ -| kind | icon | -| ------------------------ | ---- | -| `aerodrome` | | -| `adult_gaming_centre` | | -| `airfield` | | -| `alpine_hut` | | -| `amusement_ride` | | -| `animal` | | -| `art` | | -| `artwork` | | -| `atm` | | -| `attraction` | | -| `atv` | | -| `baby_hatch` | | -| `bakery` | | -| `bbq` | | -| `beauty` | | -| `bed_and_breakfast` | | -| `bench` | | -| `bicycle_parking` | | -| `bicycle_rental` | | -| `bicycle_repair_station` | | -| `boat_storage` | | -| `bookmaker` | | -| `books` | | -| `bureau_de_change` | | -| `bus_stop` | | -| `butcher` | | -| `cafe` | | -| `camp_site` | | -| `car_parts` | | -| `car_rental` | | -| `car_repair` | | -| `car_sharing` | | -| `car_wash` | | -| `car` | | -| `carousel` | | -| `cemetery` | | -| `chalet` | | -| `charging_station` | | -| `childcare` | | -| `clinic` | | -| `clothes` | | -| `college` | | -| `computer` | | -| `convenience` | | -| `customs` | | -| `dentist` | | -| `district` | | -| `doctors` | | -| `dog_park` | | -| `drinking_water` | | -| `emergency_phone` | | -| `fashion` | | -| `firepit` | | -| `fishing` | | -| `florist` | | -| `forest` | | -| `fuel` | | -| `gambling` | | -| `garden_centre` | | -| `gift` | | -| `golf_course` | | -| `golf` | | -| `greengrocer` | | -| `grocery` | | -| `guest_house` | | -| `hairdresser` | | -| `hanami` | | -| `harbourmaster` | | -| `hifi` | | -| `hospital` | | -| `hostel` | | -| `hotel` | | -| `hunting_stand` | | -| `information` | | -| `jewelry` | | -| `karaoke_box` | | -| `karaoke` | | -| `landmark` | | -| `library` | | -| `life_ring` | | -| `lottery` | | -| `marina` | | -| `maze` | | -| `memorial` | | -| `military` | | -| `mobile_phone` | | -| `money_transfer` | | -| `motorcycle_parking` | | -| `motorcycle` | | -| `national_park` | | -| `naval_base` | | -| `newsagent` | | -| `optician` | | -| `park` | | -| `parking` | | -| `perfumery` | | -| `picnic_site` | | -| `picnic_table` | | -| `pitch` | | -| `playground` | | -| `post_box` | | -| `post_office` | | -| `ranger_station` | | -| `recycling` | | -| `roller_coaster` | | -| `sanitary_dump_station` | | -| `school` | | -| `scuba_diving` | | -| `shelter` | | -| `ship_chandler` | | -| `shower` | | -| `slipway` | | -| `snowmobile` | | -| `social_facility` | | -| `stadium` | | -| `stationery` | | -| `studio` | | -| `summer_toboggan` | | -| `supermarket` | | -| `swimming_area` | | -| `taxi` | | -| `telephone` | | -| `tobacco` | | -| `toilets` | | -| `townhall` | | -| `trail_riding_station` | | -| `travel_agency` | | -| `university` | | -| `viewpoint` | | -| `waste_basket` | | -| `waste_disposal` | | -| `water_point` | | -| `water_slide` | | -| `watering_place` | | -| `wayside_cross` | | -| `wilderness_hut` | | - +### Kinds + +| kind | icon | +| ------------------------ | ------------------------------------------------ | +| `aerodrome` | | +| `adult_gaming_centre` | | +| `airfield` | | +| `alpine_hut` | | +| `amusement_ride` | | +| `animal` | | +| `art` | | +| `artwork` | | +| `atm` | | +| `attraction` | | +| `atv` | | +| `baby_hatch` | | +| `bakery` | | +| `bbq` | | +| `beach` | | +| `beauty` | | +| `bed_and_breakfast` | | +| `bench` | | +| `bicycle_parking` | | +| `bicycle_rental` | | +| `bicycle_repair_station` | | +| `boat_storage` | | +| `bookmaker` | | +| `books` | | +| `bureau_de_change` | | +| `bus_stop` | | +| `butcher` | | +| `cafe` | | +| `camp_site` | | +| `car_parts` | | +| `car_rental` | | +| `car_repair` | | +| `car_sharing` | | +| `car_wash` | | +| `car` | | +| `carousel` | | +| `cemetery` | | +| `chalet` | | +| `charging_station` | | +| `childcare` | | +| `clinic` | | +| `clothes` | | +| `college` | | +| `computer` | | +| `convenience` | | +| `customs` | | +| `dentist` | | +| `district` | | +| `doctors` | | +| `dog_park` | | +| `drinking_water` | | +| `emergency_phone` | | +| `fashion` | | +| `firepit` | | +| `fire_station` | | +| `fishing` | | +| `florist` | | +| `forest` | | +| `fuel` | | +| `gambling` | | +| `garden_centre` | | +| `gift` | | +| `golf_course` | | +| `golf` | | +| `greengrocer` | | +| `grocery` | | +| `guest_house` | | +| `hairdresser` | | +| `hanami` | | +| `harbourmaster` | | +| `hifi` | | +| `hospital` | | +| `hostel` | | +| `hotel` | | +| `hunting_stand` | | +| `information` | | +| `jewelry` | | +| `karaoke_box` | | +| `karaoke` | | +| `landmark` | | +| `library` | | +| `life_ring` | | +| `lottery` | | +| `marina` | | +| `maze` | | +| `memorial` | | +| `military` | | +| `mobile_phone` | | +| `money_transfer` | | +| `motorcycle_parking` | | +| `motorcycle` | | +| `national_park` | | +| `nature_reserve` | | +| `naval_base` | | +| `newsagent` | | +| `optician` | | +| `park` | | +| `parking` | | +| `peak` | | +| `perfumery` | | +| `picnic_site` | | +| `picnic_table` | | +| `pitch` | | +| `playground` | | +| `post_box` | | +| `post_office` | | +| `ranger_station` | | +| `recycling` | | +| `roller_coaster` | | +| `sanitary_dump_station` | | +| `school` | | +| `scuba_diving` | | +| `shelter` | | +| `ship_chandler` | | +| `shower` | | +| `slipway` | | +| `snowmobile` | | +| `social_facility` | | +| `stadium` | | +| `station` | | +| `stationery` | | +| `studio` | | +| `summer_toboggan` | | +| `supermarket` | | +| `swimming_area` | | +| `taxi` | | +| `telephone` | | +| `tobacco` | | +| `toilets` | | +| `townhall` | | +| `trail_riding_station` | | +| `travel_agency` | | +| `university` | | +| `viewpoint` | | +| `waste_basket` | | +| `waste_disposal` | | +| `water_point` | | +| `water_slide` | | +| `watering_place` | | +| `wayside_cross` | | +| `wilderness_hut` | | +| `zoo` | | ## roads Linear transportation features designed for movement, including highways, streets, - railways and piers from OpenStreetMap. This layer represents built infrastructure including railways. Refer to the [transit](#transit) layer for passenger services. +railways and piers from OpenStreetMap. This layer represents built infrastructure including railways. Refer to the [transit](#transit) layer for passenger services. -| Key | Values | Description | -| -------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | -----------: | -| `kind` | See kinds below | | -| `kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway`, `cable_car`, `pier`, `runway`, `taxiway`, `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `subway`, `tram` | | -| `ref` | string | | -| `shield_text_length` | int | | -| `network` | string | | -| `layer` | int | | -| `oneway` | string | | -| `service` | `siding`, `crossover`, `yard` | | -| `link` | int | | -| `level` | `-1`, `0`, `1` | | - +| Key | Values | Description | +| -------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | ----------: | +| `kind` | See kinds below | | +| `kind_detail` | `motorway`, `motorway_link`, `trunk`, `trunk_link`, `primary`, `primary_link`, `secondary`, `secondary_link`, `tertiary`, `tertiary_link`, `residential`, `service`, `unclassified`, `road`, `raceway`, `pedestrian`, `track`, `path`, `cycleway`, `bridleway`, `steps`, `corridor`, `sidewalk`, `crossing`, `driveway`, `parking_aisle`, `alley`, `drive-through`, `emergency_access`, `utility`, `irrigation`, `slipway`, `cable_car`, `pier`, `runway`, `taxiway`, `disused`, `funicular`, `light_rail`, `miniature`, `monorail`, `narrow_gauge`, `preserved`, `subway`, `tram` | | +| `ref` | string | | +| `shield_text_length` | int | | +| `network` | string | | +| `oneway` | string | | +| `service` | `siding`, `crossover`, `yard` | | +| `is_link` | boolean | | +| `is_tunnel` | boolean | | +| `is_bridge` | boolean | | + +### Kinds | kind | | ------------ | @@ -376,14 +380,14 @@ Lines representing scheduled passenger services suitable for rendering on the ma This layer is currently empty. -| Key | Values | Description | -| ------------- | :------: | -----------: | -| `kind_detail` | | | -| `ref` | string | | -| `network` | string | | -| `layer` | int | | -| `route` | string | | -| `service` | string | | +| Key | Values | Description | +| ------------- | :----: | ----------: | +| `kind_detail` | | | +| `ref` | string | | +| `network` | string | | +| `layer` | int | | +| `route` | string | | +| `service` | string | | ## water @@ -391,13 +395,13 @@ This layer is currently empty. Polygons from the Natural Earth 50m `lakes` and `ocean` themes for z0-z4, 10m for z5, preprocessed land polygons from [OSMCoastline](https://osmdata.openstreetmap.de) for z6+. -| Key | Values | Description | -| -------------- | :-----------------------------------------------------------------------------------------------: | -----------: | -| `kind` | `water`, `lake`, `playa`, `ocean`, `other` | | -| `kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | -| `reservoir` | boolean | | -| `alkaline` | boolean | | -| `intermittent` | boolean | | -| `bridge` | string | | -| `tunnel` | string | | -| `layer` | int | | +| Key | Values | Description | +| -------------- | :---------------------------------------------------------------------------------------------: | ----------: | +| `kind` | `water`, `lake`, `playa`, `ocean`, `other` | | +| `kind_detail` | `basin`, `canal`, `ditch`, `dock`, `drain`, `lake`, `reservoir`, `river`, `riverbank`, `stream` | | +| `reservoir` | boolean | | +| `alkaline` | boolean | | +| `intermittent` | boolean | | +| `bridge` | string | | +| `tunnel` | string | | +| `layer` | int | | diff --git a/basemaps/openlayers.md b/basemaps/openlayers.md deleted file mode 100644 index 24005b1..0000000 --- a/basemaps/openlayers.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -title: Basemaps for OpenLayers -outline: deep ---- - -# Basemaps for OpenLayers - -::: warning -This section is under construction. -::: \ No newline at end of file diff --git a/components/Icon.vue b/components/Icon.vue index 2841ab1..1cdbda2 100644 --- a/components/Icon.vue +++ b/components/Icon.vue @@ -4,15 +4,23 @@ import { ref, onMounted, onUpdated, watch } from "vue"; const canvasRef = ref(null); const props = defineProps<{ - name?: string; + kind?: string; sprites?: Promise<[any, any]>; }>(); onMounted(async () => { - const context = canvasRef.value.getContext("2d"); + const ctx = canvasRef.value.getContext("2d"); const [j, i] = await props.sprites; - const data = j[props.name]; - context.drawImage(i, data.x, data.y, data.width, data.height, 0, 0, 38, 38); + if (props.kind in j) { + const data = j[props.kind]; + ctx.drawImage(i, data.x, data.y, data.width, data.height, 0, 0, 38, 38); + } else { + ctx.strokeStyle = "steelblue"; + ctx.setLineDash([2, 2]); + ctx.strokeWidth = 2; + ctx.rect(0, 0, 38, 38); + ctx.stroke(); + } }); From 70c19f967d7b00e0566cdbbc4af81bdf74e8656b Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 4 Oct 2024 18:52:58 +0800 Subject: [PATCH 40/88] Fix localization page - remove pmap: prefixes; typos (#73) --- basemaps/layers.md | 12 +++++------ basemaps/localization.md | 44 ++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index 9f4d54a..099ba56 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -39,11 +39,11 @@ The current version is **Version 4**. ## Common Tags -| Key | Values | Description | -| ------------------------------ | ------- | ------------------------------------------ | -| `name`, `name:*`, `pgf:name:*` | string | see [Localization](/basemaps/localization) | -| `sort_rank` | integer | Importance ranking used for rendering | -| `min_zoom` | integer | Suggested zoom level to limit display | +| Key | Values | Description | +| -------------------------------------------------- | ------- | ------------------------------------------ | +| `name`, `name*`, `name:*`, `pgf:name:*`, `script*` | string | see [Localization](/basemaps/localization) | +| `sort_rank` | integer | Importance ranking used for rendering | +| `min_zoom` | integer | Suggested zoom level to limit display | ## boundaries @@ -60,7 +60,7 @@ The current version is **Version 4**. -Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains invidiual OSM equivalent buildings. +Buildings from OpenStreetMap. z0-14 contains merged buildings, even disconnected ones. z15+ contains individual OSM equivalent buildings. | Key | Values | Description | | ------------ | :-------------------------: | ------------------------------------------: | diff --git a/basemaps/localization.md b/basemaps/localization.md index 09bfcb5..34fe1e3 100644 --- a/basemaps/localization.md +++ b/basemaps/localization.md @@ -32,51 +32,51 @@ A script or writing system is the way how languages are written. For example, En If a name from OpenStreetMap, which is the de-facto primary local name, contains text in more than one script, then Protomaps breaks up the name into segments. There can be up to 3 segments: `name`, `name2`, and `name3`. Each segment should have a unique script. -Protomaps stores the scripts used for `name`, `name2`, and `name3` in separate script tags called `pmap:script`, `pmap:script2`, and `pmap:script3`. +Protomaps stores the scripts used for `name`, `name2`, and `name3` in separate script tags called `script`, `script2`, and `script3`. -If `pmap:script*` is not present on a name, then it means that the name uses the `Latin` script. +If `script*` is not present on a name, then it means that the name uses the `Latin` script. -Sometimes segmentation into single scripts fails due for example inconsistent usage of alphabets. In that case `pmap:script` is set to `Mixed`. +Sometimes segmentation into single scripts fails due for example inconsistent usage of alphabets. In that case `script` is set to `Mixed`. -In Japanese, the `Han`, `Hiragana`, and `Katakana` scripts are often mixed in one name. Should any two of these scripts appear in a name we set `pmap:script` to `Mixed-Japanese`. +In Japanese, the `Han`, `Hiragana`, and `Katakana` scripts are often mixed in one name. Should any two of these scripts appear in a name we set `script` to `Mixed-Japanese`. Let us look at some examples: #### Zürich ``` name = Zürich -(pmap:script absent) +(script absent) (name2 absent) -(pmap:script2 absent) +(script2 absent) (name3 absent) -(pmap:script2 absent) +(script3 absent) ``` -The OpenStreetMap name for "Zürich" only uses the Latin script so we export `name` and but omit `pmap:script` (implying the script of the `name` is `Latin`). +The OpenStreetMap name for "Zürich" only uses the Latin script so we export `name` and but omit `script` (implying the script of the `name` is `Latin`). #### 香港 Hong Kong ``` name = 香港 -pmap:script = Han +script = Han name2 = Hong Kong -(pmap:script2 absent) +(script2 absent) (name3 absent) -(pmap:script3 absent) +(script3 absent) ``` -The OpenStreetMap name for Hong Kong is "香港 Hong Kong". We break this up into `name` and `name2` in Protomaps. Since the script of `name2` is `Latin`, the `pmap:script2` tag is omitted. The script of `name` is `Han` which is encoded in `pmap:script`. +The OpenStreetMap name for Hong Kong is "香港 Hong Kong". We break this up into `name` and `name2` in Protomaps. Since the script of `name2` is `Latin`, the `script2` tag is omitted. The script of `name` is `Han` which is encoded in `script`. #### Casablanca ⵜⵉⴳⵎⵉ ⵜⵓⵎⵍⵉⵍⵜ الدار البيضاء ``` name = Casablanca -(pmap:script absent) +(script absent) name2 = ⵜⵉⴳⵎⵉ ⵜⵓⵎⵍⵉⵍⵜ -pmap:script2 = Tifinagh +script2 = Tifinagh name3 = الدار البيضاء -pmap:script3 = Arabic +script3 = Arabic ``` -Casablanca in OpenStreetMap is stored as "Casablanca ⵜⵉⴳⵎⵉ ⵜⵓⵎⵍⵉⵍⵜ الدار البيضاء". In Protomaps we break this label up into 3 parts. Since the text in `name` uses the `Latin` script, we omit the `pmap:script` tag. The other two parts use the Tifinagh and Arabic script. +Casablanca in OpenStreetMap is stored as "Casablanca ⵜⵉⴳⵎⵉ ⵜⵓⵎⵍⵉⵍⵜ الدار البيضاء". In Protomaps we break this label up into 3 parts. Since the text in `name` uses the `Latin` script, we omit the `script` tag. The other two parts use the Tifinagh and Arabic script. ## Translated Names @@ -167,9 +167,9 @@ Extending our Switzerland example with exonym and endonym from other languages: | Urdu | اردو | `name:ur` | `Arabic` | | Vietnamese | Tiếng Việt | `name:vi` | `Latin` | -NOTE: `Mixed-Japanese` is a custom `pmap:script` value used for labels that contain Hiragana or Katakana mixed with a second or third script. In Japanese, these two scripts often appear in combination with others. +NOTE: `Mixed-Japanese` is a custom `script` value used for labels that contain Hiragana or Katakana mixed with a second or third script. In Japanese, these two scripts often appear in combination with others. -NOTE 2 : Values in `pmap:script*` follow the [Unicode Standard Annex #24: Script Names](https://www.unicode.org/reports/tr24/). +NOTE 2 : Values in `script*` follow the [Unicode Standard Annex #24: Script Names](https://www.unicode.org/reports/tr24/). ## Styling @@ -196,13 +196,13 @@ For a map localized to Greek, we would use `name:el = Μιλάνο` in the first Milano ``` -## Positioned glyph font `pmap:pgf:name:*` values +## Positioned glyph font `pgf:name:*` values -Protomaps adds additional names for a small set of language scripts, currently just the `Devanagari` script used for Hindi (`name:hi` and `pmap:pgf:name:hi`) and related languages. +Protomaps adds additional names for a small set of language scripts, currently just the `Devanagari` script used for Hindi (`name:hi` and `pgf:name:hi`) and related languages. Rendering text in web browsers works for almost all languages and scripts and feels like magic. However, specialized map renderers like MapLibre have to reimplement text rendering and text layout which is complicated when text needs to be curved along linear map features instead of placed only horizontally or vertically. MapLibre normally assumes a one-to-one mapping between glyphs and Unicode codepoints that also exist in MapLibre font files (aka "font stacks") to accomplish the layout for a large but limited number of scripts. Plugins have been developed to extend MapLibre for **right-to-left** scripts like Arabic and Hebrew, and MapLibre has built-in support for **CJK scripts** like Chinese, Japanese, and Korean. -To facilitate Protomap's support of additional, non-supported scripts in MapLibre (like the Devanagari script used by the Hindi language), Protomaps exports names with "positioned glyphs" so MapLibre can use codepoints as indices of positioned glyphs in an additional custom "font stack". While the raw `pmap:pgf:name:*` values look like gibberish when inspecting the raw values, they render correctly in MapLibre to the end user. +To facilitate Protomap's support of additional, non-supported scripts in MapLibre (like the Devanagari script used by the Hindi language), Protomaps exports names with "positioned glyphs" so MapLibre can use codepoints as indices of positioned glyphs in an additional custom "font stack". While the raw `pgf:name:*` values look like gibberish when inspecting the raw values, they render correctly in MapLibre to the end user. See more: @@ -231,7 +231,7 @@ NOTE: Right-to-left scripts and languages like Arabic and Hebrew requires a spec #### MapLibre partial support -Requires paired positioned glyph font [font stack](https://maplibre.org/maplibre-style-spec/glyphs/) paired with `pmap:pgf:name:*` values. The PGF fontstacks used by the Protomaps basemaps are available at https://github.com/protomaps/basemaps-assets/tree/main/fonts. +Requires paired positioned glyph font [font stack](https://maplibre.org/maplibre-style-spec/glyphs/) paired with `pgf:name:*` values. The PGF fontstacks used by the Protomaps basemaps are available at https://github.com/protomaps/basemaps-assets/tree/main/fonts. | Script | Languages | | ------- | ---------| From 9206bd29636f4c25b54ebee791877bda2f9a58a9 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Tue, 8 Oct 2024 13:05:00 +0800 Subject: [PATCH 41/88] Deploy index azure (#74) * deploy chart: add azure --- deploy/index.md | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/deploy/index.md b/deploy/index.md index 55d1d8e..3de4f32 100644 --- a/deploy/index.md +++ b/deploy/index.md @@ -50,15 +50,15 @@ A CDN deployment of Protomaps has three parts: _Cloudflare is recommended for budget minded beginners. AWS is recommended for commercial companies who require a faster map and/or can offset increased marginal cost with monetization._ -| feature | static pmtiles | Cloudflare | AWS | GCP | [Caddy](/deploy/server#caddy) | `pmtiles serve` | -| - | :-: | :-:| :-: | :-: | :-: | - | -| Z/X/Y compatible | ✖️ | ✅ | ✅ | ✅ | ✅ | ✅ | -| Edge caching | ✖️ | ✅ | ✅ | With CDN | With CDN | With CDN | -| SSL | ✅ | ✅ | ✅ | ✅ | ✅ | With reverse proxy | -| Scale to zero | ✅ | $5 (USD) | ✅ | ✅ | ✖️ | ✖️ | -| Setup effort | 😓 | 😓 | 😓 | 😓😓 | 😓😓 | 😓😓😓 | -| Latency | 🚀 | 🚀 | 🚀🚀🚀 | 🚀🚀🚀 | 🚀🚀 | 🚀🚀🚀 | -| Cost | 💰 | 💰 | 💰💰💰 | 💰💰💰 | 💰💰 | 💰💰 | +| feature | static pmtiles | Cloudflare | AWS | GCP, Azure | [Caddy](/deploy/server#caddy) | `pmtiles serve` | +| ---------------- | :--------------: | :----------: | :---: | :----------: | :-----------------------------: | ------------------ | +| Z/X/Y compatible | ✖️ | ✅ | ✅ | ✅ | ✅ | ✅ | +| Edge caching | ✖️ | ✅ | ✅ | With CDN | With CDN | With CDN | +| SSL | ✅ | ✅ | ✅ | ✅ | ✅ | With reverse proxy | +| Scale to zero | ✅ | $5 (USD) | ✅ | ✅ | ✖️ | ✖️ | +| Setup effort | 😓 | 😓 | 😓 | 😓😓 | 😓😓 | 😓😓😓 | +| Latency | 🚀 | 🚀 | 🚀🚀🚀 | 🚀🚀🚀 | 🚀🚀 | 🚀🚀🚀 | +| Cost | 💰 | 💰 | 💰💰💰 | 💰💰💰 | 💰💰 | 💰💰 | ### Feature explanation @@ -69,7 +69,3 @@ _Cloudflare is recommended for budget minded beginners. AWS is recommended for c - **Setup effort**: Developer time to configure complete cloud tile serving solution, less effort 😓 is better than more 😓😓😓 effort - **Latency**: Speedy maps tiles 🚀🚀🚀 load in ≤ 200 ms in the client for customers, slow tiles 🚀 load ≥ 500 ms - **Cost**: Total cost to run tile serving system, with 💰 being cheaper at $5 USD and 💰💰💰 more expensive options that including storage egress and/or CDN bandwidth costs to achieve lower latency - -### Coming soon - -- **Azure** support is in development and planned for 2024. From 89c19f9c27864774d5cb49e9e217afe11a61c3bb Mon Sep 17 00:00:00 2001 From: Oliver Wipfli Date: Wed, 9 Oct 2024 07:15:54 +0200 Subject: [PATCH 42/88] Add RTL plugin (#76) * Add RTL plugin --- components/MaplibreMap.vue | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index 1fd68af..fab3e72 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -103,6 +103,12 @@ const props = defineProps<{ }>(); onMounted(() => { + if (maplibregl.getRTLTextPluginStatus() === "unavailable") { + maplibregl.setRTLTextPlugin( + "https://unpkg.com/@mapbox/mapbox-gl-rtl-text@0.2.3/mapbox-gl-rtl-text.js", + false, + ); + } map = new maplibregl.Map({ container: mapRef.value, style: style(props.theme, props.highlightLayer), From 8b2c87115546d2f74b3d81d757d2369208f16e63 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 9 Oct 2024 14:13:35 +0800 Subject: [PATCH 43/88] icons display in dark mode (#77) * icons display in dark mode --- basemaps/layers.md | 2 +- components/Icon.vue | 30 ++++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index 099ba56..9b63caa 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -8,7 +8,7 @@ outline: deep import Icon from '../components/Icon.vue' const fetchImage = async (url) => { - new Promise((resolve,reject) => { + return new Promise((resolve,reject) => { if (typeof window !== 'undefined') { const img = new Image(); img.onload = () => { resolve(img); } diff --git a/components/Icon.vue b/components/Icon.vue index 1cdbda2..8eff5cf 100644 --- a/components/Icon.vue +++ b/components/Icon.vue @@ -1,26 +1,40 @@ From 205dac2cbb93bc5daf91d4672aaca3d8ea124feb Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Wed, 9 Oct 2024 18:55:35 +0800 Subject: [PATCH 44/88] read sprites from v4 (#78) --- basemaps/layers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/basemaps/layers.md b/basemaps/layers.md index 9b63caa..00e4a45 100644 --- a/basemaps/layers.md +++ b/basemaps/layers.md @@ -19,7 +19,7 @@ outline: deep }) } - const SPRITES_BASE = "https://protomaps.github.io/basemaps-assets/sprites/v3"; + const SPRITES_BASE = "https://protomaps.github.io/basemaps-assets/sprites/v4"; const sprites = Promise.all([ fetch(`${SPRITES_BASE}/light@2x.json`).then(resp => resp.json()), From b095c0891b95b6120ea324a574e37d1f0a1d8e5e Mon Sep 17 00:00:00 2001 From: mizmay <3074691+mizmay@users.noreply.github.com> Date: Wed, 9 Oct 2024 19:06:56 -0700 Subject: [PATCH 45/88] Update maplibre.md (#79) --- pmtiles/maplibre.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pmtiles/maplibre.md b/pmtiles/maplibre.md index 38652c9..b1042cf 100644 --- a/pmtiles/maplibre.md +++ b/pmtiles/maplibre.md @@ -9,7 +9,7 @@ outline: deep For reading PMTiles directly from cloud storage, you'll need the `pmtiles` JavaScript library. -The JavaScript library includes a plugin for MapLibre GL that uses its [`addProtocol` feature.](https://maplibre.org/maplibre-gl-js-docs/api/properties/#addprotocol) +The JavaScript library includes a plugin for MapLibre GL that uses its [`addProtocol` feature.](https://maplibre.org/maplibre-gl-js/docs/API/functions/addProtocol/) ```bash npm install pmtiles From 64d7bfa0d66f4dccc4406130f639957ee6c3642d Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Thu, 10 Oct 2024 23:53:08 +0800 Subject: [PATCH 46/88] styles updates for v4.1.0 (#80) --- basemaps/maplibre.md | 4 ++-- basemaps/themes.md | 14 +++++++------- components/MaplibreMap.vue | 2 +- guide/security-privacy.md | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/basemaps/maplibre.md b/basemaps/maplibre.md index ec81083..afe7c10 100644 --- a/basemaps/maplibre.md +++ b/basemaps/maplibre.md @@ -29,7 +29,7 @@ You can view a list of available fonts [in the GitHub repository](https://github The `sprite` key references a URL specific to one of [the default themes](/basemaps/themes): ```js -sprite: "https://protomaps.github.io/basemaps-assets/sprites/v3/light" +sprite: "https://protomaps.github.io/basemaps-assets/sprites/v4/light" ``` These are required for townspots, highway shields and point of interest icons. @@ -56,7 +56,7 @@ import layers from 'protomaps-themes-base'; style: { version: 8, glyphs:'https://protomaps.github.io/basemaps-assets/fonts/%7Bfontstack%7D/%7Brange%7D.pbf', - sprite: "https://protomaps.github.io/basemaps-assets/sprites/v3/light", + sprite: "https://protomaps.github.io/basemaps-assets/sprites/v4/light", sources: { "protomaps": { type: "vector", diff --git a/basemaps/themes.md b/basemaps/themes.md index 729fad3..1476d97 100644 --- a/basemaps/themes.md +++ b/basemaps/themes.md @@ -15,30 +15,30 @@ These examples use the preferred [MapLibre GL JS](/basemaps/maplibre) library. ### light -A general-purpose basemap style. +A general-purpose basemap style with icons. - + ### dark -A general-purpose basemap style. +A general-purpose basemap style with icons. - + ### white A style for data visualization. - + ### grayscale A style for data visualization. - + ### black A style for data visualization. - \ No newline at end of file + \ No newline at end of file diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index fab3e72..65f6627 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -77,7 +77,7 @@ const style = (passedTheme?: string, highlightLayer?: string) => { version: 8, glyphs: "https://protomaps.github.io/basemaps-assets/fonts/%7Bfontstack%7D/%7Brange%7D.pbf", - sprite: `https://protomaps.github.io/basemaps-assets/sprites/v3/${theme}`, + sprite: `https://protomaps.github.io/basemaps-assets/sprites/v4/${theme}`, sources: { protomaps: { type: "vector", diff --git a/guide/security-privacy.md b/guide/security-privacy.md index 004f0ea..e1a4451 100644 --- a/guide/security-privacy.md +++ b/guide/security-privacy.md @@ -110,7 +110,7 @@ Below is a complete example of a map application that avoids third-party data pr center: [11.24962,43.77078], style: { glyphs: "fonts/{fontstack}/{range}.pbf", - sprite: "sprites/v3/light", + sprite: "sprites/v4/light", version: 8, sources: { protomaps: { diff --git a/package-lock.json b/package-lock.json index 1a0f0f1..f1f5627 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "dependencies": { "maplibre-gl": "^4.7.1", "pmtiles": "^3.2.0", - "protomaps-themes-base": "4.0.1", + "protomaps-themes-base": "4.1.0", "vitepress": "^1.0.0-rc.15" }, "devDependencies": { @@ -1453,9 +1453,9 @@ "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==" }, "node_modules/protomaps-themes-base": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/protomaps-themes-base/-/protomaps-themes-base-4.0.1.tgz", - "integrity": "sha512-p7uWeV8AWVqNs4Jj3ZaO/ZMDI52fRSEbLe9dIGfGgcHjy7M1u+HsS06WsOWNnmU+i+2rOSSkF0LNZuPQ+6/w+w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/protomaps-themes-base/-/protomaps-themes-base-4.1.0.tgz", + "integrity": "sha512-JHIaJB9W7lfbOgWCbohjnl3LjpDqBlN/8T3WFWeeLMkXpzz6ab9/Bdg1kdoryE0clevHMjv0tUAHnwsf937sQw==", "license": "BSD-3-Clause" }, "node_modules/quickselect": { diff --git a/package.json b/package.json index 9fce5dd..503e36d 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "dependencies": { "maplibre-gl": "^4.7.1", "pmtiles": "^3.2.0", - "protomaps-themes-base": "4.0.1", + "protomaps-themes-base": "4.1.0", "vitepress": "^1.0.0-rc.15" }, "devDependencies": { From e7331e53a46580b8287f4a6c6da1a6c26e5f4b68 Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 11 Oct 2024 00:52:59 +0800 Subject: [PATCH 47/88] add language selector to localization page (#81) --- basemaps/localization.md | 2 +- components/MaplibreMap.vue | 187 ++++++++++++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 5 deletions(-) diff --git a/basemaps/localization.md b/basemaps/localization.md index 34fe1e3..b271ae6 100644 --- a/basemaps/localization.md +++ b/basemaps/localization.md @@ -10,7 +10,7 @@ outline: deep Protomaps has several localization options for names used in text labels. - + ## Local Names diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index 65f6627..7e32a14 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -9,6 +9,8 @@ const { isDark } = useData(); const mapRef = ref(null); var map; +const lang = ref("en"); + const tableFromProps = (props: unknown) => { let tableHTML = ""; @@ -63,7 +65,7 @@ const highlightLayers = (sourceName: string, highlightName?: string) => { ]; }; -const style = (passedTheme?: string, highlightLayer?: string) => { +const style = (passedTheme?: string, highlightLayer?: string, lang?: lang) => { const theme = passedTheme || (isDark.value @@ -87,7 +89,7 @@ const style = (passedTheme?: string, highlightLayer?: string) => { transition: { duration: 0, }, - layers: layers("protomaps", theme).concat( + layers: layers("protomaps", theme, lang).concat( highlightLayers("protomaps", highlightLayer), ), }; @@ -100,6 +102,7 @@ const props = defineProps<{ zoom?: number; lat?: number; lng?: number; + langSelect?: boolean; }>(); onMounted(() => { @@ -145,12 +148,188 @@ onMounted(() => { ); }); -watch(isDark, () => { - map.setStyle(style(props.theme, props.highlightLayer)); +watch([isDark, lang], () => { + map.setStyle(style(props.theme, props.highlightLayer, lang.value)); }); + +const language_script_pairs = [ + { + lang: "ar", + full_name: "Arabic", + }, + { + lang: "cs", + full_name: "Czech", + }, + { + lang: "bg", + full_name: "Bulgarian", + }, + { + lang: "da", + full_name: "Danish", + }, + { + lang: "de", + full_name: "German", + }, + { + lang: "el", + full_name: "Greek", + }, + { + lang: "en", + full_name: "English", + }, + { + lang: "es", + full_name: "Spanish", + }, + { + lang: "et", + full_name: "Estonian", + }, + { + lang: "fa", + full_name: "Persian", + }, + { + lang: "fi", + full_name: "Finnish", + }, + { + lang: "fr", + full_name: "French", + }, + { + lang: "ga", + full_name: "Irish", + }, + { + lang: "he", + full_name: "Hebrew", + }, + { + lang: "hi", + full_name: "Hindi", + }, + { + lang: "hr", + full_name: "Croatian", + }, + { + lang: "hu", + full_name: "Hungarian", + }, + { + lang: "id", + full_name: "Indonesian", + }, + { + lang: "it", + full_name: "Italian", + }, + { + lang: "ja", + full_name: "Japanese", + }, + { + lang: "ko", + full_name: "Korean", + }, + { + lang: "lt", + full_name: "Lithuanian", + }, + { + lang: "lv", + full_name: "Latvian", + }, + { + lang: "ne", + full_name: "Nepali", + }, + { + lang: "nl", + full_name: "Dutch", + }, + { + lang: "no", + full_name: "Norwegian", + }, + { + lang: "mr", + full_name: "Marathi", + }, + { + lang: "mt", + full_name: "Maltese", + }, + { + lang: "pl", + full_name: "Polish", + }, + { + lang: "pt", + full_name: "Portuguese", + }, + { + lang: "ro", + full_name: "Romanian", + }, + { + lang: "ru", + full_name: "Russian", + }, + { + lang: "sk", + full_name: "Slovak", + }, + { + lang: "sl", + full_name: "Slovenian", + }, + { + lang: "sv", + full_name: "Swedish", + }, + { + lang: "tr", + full_name: "Turkish", + }, + { + lang: "uk", + full_name: "Ukrainian", + }, + { + lang: "ur", + full_name: "Urdu", + }, + { + lang: "vi", + full_name: "Vietnamese", + }, + { + lang: "zh-Hans", + full_name: "Chinese (Simplified)", + }, + { + lang: "zh-Hant", + full_name: "Chinese (Traditional)", + }, +]; From 1d8e016dcc7c1024bdd07645941111619e5b05bf Mon Sep 17 00:00:00 2001 From: Brandon Liu Date: Fri, 11 Oct 2024 11:08:24 +0800 Subject: [PATCH 48/88] localization: sort list of languages (#82) --- components/MaplibreMap.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/MaplibreMap.vue b/components/MaplibreMap.vue index 7e32a14..880c08f 100644 --- a/components/MaplibreMap.vue +++ b/components/MaplibreMap.vue @@ -317,7 +317,7 @@ const language_script_pairs = [ lang: "zh-Hant", full_name: "Chinese (Traditional)", }, -]; +].sort((a, b) => a.full_name.localeCompare(b.full_name));