From 50d0aa7420eb7e347229b52849b1c7fd8d4f4348 Mon Sep 17 00:00:00 2001 From: "Nathaniel V. KELSO" Date: Thu, 20 Jul 2023 00:50:44 -0700 Subject: [PATCH] Round 2 enhancements for Felt (#47) * bug fix for town, village as city level features (not neighbourhood) * init Makefile targets * add pmap:kind coallesce * converge towards shield_text_length; pass thru other network values; set pmap:kind_detail on other kind * refactor pmap:kind calculation and add pmap:kind_detail * refactor places, add pmap:min_zoom, add sort func, add label grid * rework low zoom NE water * stub out water label points (natural earth only) * fix test tile coord; tmp pmtiles bin location * add kind = national_park logic * add derived water polygon labels in physical points layer * fix java errors, comment out setSortKey for later debug * reorder sections for a-z legibility, add optional pmap:kind_detail property coallese and extra tags (#39), show some POIs earlier depending on kind or area, special handling for kind = national_park * add washington state target * show county lines earlier, export Tilezen kinds (and region not state) * block {building|building:part}=no; push building:part to later zooms; merge buildings at z14; note future height quantization * do NOT export names; formatting * do NOT export names, or areas * export label points for water polygons; & etc * use Tilezen kind values (locality not city) * export POIs for ways, rels too (area graded per zoom); some kind of boundary and landuse; national_park kind; add kind_detail; special case no name POI zooms; special case other minor kind zooms * fix service minor roads; fix sidewalks * show piers later; drop short featues * fix NE layer parsing; do NOT export names (see physical points layer instead) * quantize heights by zoom; push building_part to later zooms * always export pmap:kind * add national_park, protected_area, and nature_reserve (moved from natural layer); show at earlier zooms; more sophisticated kind setting; merge features at early zooms * remove national_park, protected_area, nature_reserve (moved to landuse layer); merge features at early zooms * show rivers earlier; always set kind value; intermittent to boolean * switch to min_label for NE instead of min_zoom; update zoom area grading; export kind_detail * prefix population_rank with pmap: * bug fixes for kind calculations; better way area calculation; show larger area features earlier by area and zoom and kind; show taller height kinds earlier; export min_zoom for label collision * show motorway and primary earlier; show service roads later; show paths later; do not remove links early * show airport runway, taxiway earlier; show some types of rail later; smarter kind calculation * update kind calculation; boolean exports * prefix population_rank with pmap: * remove debug attr * add phony list; remove complicated targets * add beach, forest, military, naval_base, airfield, zoo kinds; bug fix national_park; reorder poly var creation later; stop merging tile features * move kind calc above; Tilezen schema notes; min pixel size to 2 from 3 * some tiny lake labels earlier; add Tilezen schema comments * refactor property gathering logic and move setAttr later; add Tilezen schema notes * add attraction kinds, beach kind, hide early node university kind, add (national) forest kind, bug fix for national_park; add few tier 1 landuse kinds to early zoom allowlist; fix height zoom grading bug; hide mid-zoom hotels (from height); Tilezen schema notes * refactor property gathering and attr setting; Tilezen schema notes * Tilezen schema notes; hide names from early zooms for more merging * refactor property gathering and attr setting; Tilezen schema notes * remove unused imports, fix silly Java error * add to early zoom blocklist * update style for schema changes * add SF target * ensure pier pass thru * add grass * add label grid, separate NE from OSM at zoom 7 * add label grid; bug fixes for kinds and zooms (per kind) * add planet target * add new targets * use low zoom boundaries from Natural Earth * add zoom logic for country, region * guard against nulls * guard against null names; spell out more country names * guard against null names * perf; rework NE kind and kind_detail * npe, again; cleanup imports and var names * standardize on var names * ensure odd numbered admin_level do not export * add brunnel pmap:level, remove dedicated props * add brunnel; push disused lines later * add quarter back * standardize sf var name * push small parks later; university buildings later * brunnel; standardize sf var name; kind_detail * regrade park area > zooms * add changelog, version, and semver statement * similar casing order change as #42 for merge conflicts * update 1.0.0 date --------- Co-authored-by: Brandon Liu --- CHANGELOG.md | 154 +++ SEMANTIC-VERSIONING.md | 200 +++ VERSION | 1 + base/src/base_layers.ts | 692 ++++++++-- base/src/colors.ts | 12 + compare/Makefile | 4 + tiles/Makefile | 88 ++ .../java/com/protomaps/basemap/Basemap.java | 2 + .../basemap/feature/CountryNameZooms.java | 1216 +++++++++++++++++ .../protomaps/basemap/feature/FeatureId.java | 6 +- .../basemap/feature/RegionNameZooms.java | 316 +++++ .../protomaps/basemap/layers/Boundaries.java | 212 ++- .../protomaps/basemap/layers/Buildings.java | 101 +- .../com/protomaps/basemap/layers/Earth.java | 7 +- .../com/protomaps/basemap/layers/Landuse.java | 184 ++- .../com/protomaps/basemap/layers/Natural.java | 40 +- .../basemap/layers/PhysicalLine.java | 43 +- .../basemap/layers/PhysicalPoint.java | 180 ++- .../com/protomaps/basemap/layers/Places.java | 186 ++- .../com/protomaps/basemap/layers/Pois.java | 468 ++++++- .../com/protomaps/basemap/layers/Roads.java | 186 ++- .../com/protomaps/basemap/layers/Transit.java | 90 +- .../com/protomaps/basemap/layers/Water.java | 143 +- .../com/protomaps/basemap/names/NeNames.java | 8 +- .../com/protomaps/basemap/names/OsmNames.java | 6 +- 25 files changed, 4165 insertions(+), 380 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 SEMANTIC-VERSIONING.md create mode 100644 VERSION create mode 100644 compare/Makefile create mode 100644 tiles/Makefile create mode 100644 tiles/src/main/java/com/protomaps/basemap/feature/CountryNameZooms.java create mode 100644 tiles/src/main/java/com/protomaps/basemap/feature/RegionNameZooms.java diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..90784871 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,154 @@ +v3.0.0-pre1 +------ +- **Release date:** 2023-07-10. +- **Credits:** [@nvkelso](https://github.com/nvkelso), [@bdon](https://github.com/bdon), [@jamesscottbrown](https://github.com/jamesscottbrown), [@tyrauber](https://github.com/tyrauber), [@HeikoGr](https://github.com/HeikoGr), and [@Edefritz](https://github.com/Edefritz) + +#### BREAKING CHANZGES + +- **landuse** layer: Move `national_park`, `protected_area`, and `nature_reserve` to landuse layer from natural layer as they are not natural but cultural. [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: OSM features with `leisure` tag are now mapped to individual kind values instead of all erroneously to `park`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Features with `amenity` tag are mapped to individual kind values instead of all erroneously to `school` when not `hospital`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural** layer: Moved `national_park`, `protected_area`, and `nature_reserve` to **landuse** layer. [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: OSM `state` (province) is now `region` (preparing for v4 Tilezen changes) [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: OSM `city` is now `locality` (preparing for v4 Tilezen changes) [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: OSM `town` and `village` are now `locality` ("city") instead of `neighbourhood` (fixing v2 series bug and preparing for v4 Tilezen changes) [#37](https://github.com/protomaps/basemaps/pull/37) + +#### ENHANCEMENTS + +- **Significant performance improvements** to reduce p99 file sizes globally (-XX%) at all zooms to under 200 kb. Changes include: ... +- **Core Tilezen schema properties added:**: + * `pmap:kind` is present on every feature now in every layer + * `pmap:kind_detail` is optionally present on some features in some layers + * **DEPRECATION WARNING**: in v4, the `pmap:` prefix will be removed (so `pmap:kind` will become simply `kind`) +- **Core OSM tags for different kinds of places have been augmented, but...** + * **DEPRECATION WARNING**: These OSM tags are marked for deprecation in v4 schema, do not use these for styling. They aren't needed and take up extra file size. + * If an explicate value is needed it should be a kind, or included in kind_detail. If there is a gap, please file an issue so it can be addressed. +- **Less names, now with label placements:** + * Names have been removed from most features at early and mid-zooms (to also reduce file size) + * Names have been kept on some features at early- and mid-zooms when they are known to be used for map display + * When features do have names, a pmap:min_zoom is added to achieve more predictable label collisions client side. + * **DEPRECATION WARNING**: in v4, the `pmap:` prefix will be removed (so `pmap:min_zoom` will become simply `min_zoom`) +- **Zoom ranges** of most features have been adjusted to remove details (and reduce file size) at early and mid-zooms. [#47](https://github.com/protomaps/basemaps/pull/47) +- **boundaries** layer: Add boolean indication of `disputed` lines [#37](https://github.com/protomaps/basemaps/pull/37) +- **boundaries** layer: Use Natural Earth for low zoom boundary lines, including disputed status [#47](https://github.com/protomaps/basemaps/pull/47) +- **boundaries** layer: Don't export **admin_level** `1`, `3`, `5`, and `7` – those generally aren't styled and are taking up a lot of file size even at very low zooms [#47](https://github.com/protomaps/basemaps/pull/47) +- **boundaries** layer: Adjust zoom range of `county` lines to show starting at zoom 8 instead of 10. [#47](https://github.com/protomaps/basemaps/pull/47) +- **buildings** layer: Adjust zoom ranges, push `building_part` kind features to later zooms (to reduce file size) [#47](https://github.com/protomaps/basemaps/pull/47) +- **buildings** layer: Quantize `height` tag at mid-zooms so more buildings merge in post-processing (to reduce file size) [#47](https://github.com/protomaps/basemaps/pull/47) +- **buildings** layer: Add optional `min_height` property to enable 2.5D and 3D visualizations [#47](https://github.com/protomaps/basemaps/pull/47) +- **earth** layer: Add 8 px buffer for Natural Earth sourced features at low zooms. [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Add new `kind` values for: `beach`, `pier`, `zoo`, `military`, `naval_base`, `airfield`, `cemetery`, `recreation_ground`, `winter_sports`, `quarry`, `park`, and `forest`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Add new `national_park` kind (versus `park`), looking at `operator` tag to derive this from OSM to emphasize US National Park Service in United States of America and elsewhere [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Improve detection of `forest` kind (versus `wood`), looking at `operator` tag to derive this from OSM to emphasize National Forests in United States of America and elsewhere [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Remove names to reduce tile sizes (see the **pois** layer for calculated label points) [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Reduce small polygons to improve figure-ground contrast and reduce file size. [#47](https://github.com/protomaps/basemaps/pull/47) +- **landuse** layer: Add `boundary`, `landuse`, `leisure`, and `natural` properties from OSM tags (though don't use them to be v4 safe) [#47 ](https://github.com/protomaps/basemaps/pull/47 ) +- **natural** layer: Add new `kind` value for `grass` [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural** layer: Update to the same `pmap:kind` coallesce as in the **landuse** layer [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural** layer: Remove `boundary` and `leisure` properties (they moved to **landuse** layer along with the relevant featurse) [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural** layer: Remove small polygons to improve figure-ground contrast and reduce file size. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural** layer: Merge polygons with similar attributes to reduce file size. [#37](https://github.com/protomaps/basemaps/pull/37) +- **natural_lines** layer: Show `river` lines 3 zooms earlier at zoom 9. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_lines** layer: For linear water features like rivers – add tunnel / bridge indicator with `pmap:level` (same as **roads** layer) [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_lines** layer: Add `bridge`, `tunnel`, and `layer` properties from OSM. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_lines** layer: Add `intermittent` boolean indicators. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_points** layer: Show `ocean` and `sea` label points much earlier. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_points** layer: Add calculated label positions for water features (like `lake`, `reservoir`, `swimming_pool` and other terrestial water features; and `bay`, `strait`, `fjord` marine featuers) [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_points** layer: Add `alkaline`, `intermittent`, and `reservoir` boolean indicators. [#47](https://github.com/protomaps/basemaps/pull/47) +- **natural_points** layer: Add `natural`, `landuse`, `leisure`, `water`, and `waterway` properties from OSM (though don't use them to be v4 safe) [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Use Natural Earth for low-zoom `locality` features (to reduce file size) [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Add server-side label collisions with a label grid to reduce number of places in tiles, especially at mid-zooms. [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Remove `country` and `region` labels from mid- and high-zooms (still present at low-zooms) [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Add `pmap:kind_detail` for original OSM "place" tag values (including "city" instead of "locality") [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Add `pmap:population_rank` for a quantized and backfilled population approximation. [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Curate custom `min_zooms` for `country` and `region` (state/province) labels to removes many labels from early zooms when they couldn't reasonably be labeled anyhow (to reduce file size) [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add new `national_park` kind (versus `park`), looking at `operator` tag to derive this from OSM to emphasize US National Park Service in United States of America and elsewhere [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Improve detection of `forest` kind (versus `wood`), looking at `operator` tag to derive this from OSM to emphasize National Forests in United States of America and elsewhere [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add allow listed OSM features from natural (`beach`) and landuse (`cemetery`, `recreation_ground`, `winter_sports`, `quarry`, `park`, `forest`, `military`) tags. [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add `amenity`, `attraction`, `boundary` (select), `craft`, `historic`, `landuse` (select), `natural` (select), `shop`, `railway` (select), and `tourism` features and exported OSM tag to schema property (though don't use them to be v4 safe) [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add additional pasthru unrestricted OSM values from `attraction`, `craft`, `historic`, `landuse`, `leisure`, and `natural` tags. This augments `amenity`, `railway`, `shop`, and `tourism`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add `cuisine`, `religion` tags (though use `pmap:kind_detail` instead to be v4 safe) [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add `iata` property on `airport` kind features to indicate if they have international service. [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Derive label centroids from OSM ways and relations features to hugely increasing the number of included features [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add server-side label collisions with a label grid to reduce number of features in tiles at mid-zooms (all features still included at `max_zoom`) [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Add smattering of higher priority (even within a kind) features at earlier zoom levels (eg based on feature area and/or height). This primarily effects `aerodrome`, `airfield`, `cemetery`, `college`, `forest`, `golf_course`, `grocery`, `hospital`, `library`, `marina`, `military`, `national_park`, `nature_reserve`, `naval_base`, `park`, `post_office`, `protected_area`, `stadium`, `supermarket`, `townhall`, `university`, and `zoo`, or very large building area derived labels, or very tall height building area derived labels. [#47](https://github.com/protomaps/basemaps/pull/47) +- **pois** layer: Indicate with `pmap:min_zoom` property when a feature first became eligible to be in tiles, and mark `max_zoom` features to hide until later display zooms. Use this `pmap:min_zoom` property to setup client-side labe collisions. NOTE: In v4 it'll be renamed to just `min_zoom`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **roads** layer: Modify `ref` tag values to remove some prefix values and remove whitespace values (for easier construction into client side shields with narrower graphics). [#37](https://github.com/protomaps/basemaps/pull/37) +- **roads** layer: Add `shield_text_length` for the integer lenth of the `ref` tag (with transformations) to more quickly converge towards Tilezen syntax. Can be paired with new `network` property to display client-side road shields. [#37](https://github.com/protomaps/basemaps/pull/37) +- **roads** layer: Add `network` with values of `US:I`, `US:US` or `other`. Can be paired with new `shield_text_length` and `ref` properties to display client-side road shields. [#37](https://github.com/protomaps/basemaps/pull/37) +- **roads** layer: Add `pmap:kind_detail` for values of `service` tag for `other` kind roads (eg for `parking_aisle` features) [#47](https://github.com/protomaps/basemaps/pull/47) +- **roads** layer: Remove name and ref tags from low- and mid-zooms selectively by road class to improve merging and reduce file size. [#47 ](https://github.com/protomaps/basemaps/pull/47 ) +- **roads** layer: Add `pier` kind lines. See also new `pier` areas in the **landuse** layer. [#47](https://github.com/protomaps/basemaps/pull/47) +- **roads** layer: Remove small lines at low- and mid-zooms to improve figure-ground contrast and reduce file size. [#37](https://github.com/protomaps/basemaps/pull/37) +- **transit** layer: Show `runway` and `taxiway` kinds earlier (zoom 9 and 10), show `pier`, `yard`, `siding`, and `crossover` kinds later (zoom 13). [#47 ](https://github.com/protomaps/basemaps/pull/47 ) +- **transit** layer: Show certain kinds of railway (like `light_rail` and `disused`) later. [#47](https://github.com/protomaps/basemaps/pull/47) +- **transit** layer: Indicate with `pmap:kind_detail` the value of the relevant `service`, `ferry`, or `aerialway` tags. [#47](https://github.com/protomaps/basemaps/pull/47) +- **transit** layer: Add tunnel / bridge indicators with `pmap:level` (same as roads layer in v2), and `layer` property. [#47](https://github.com/protomaps/basemaps/pull/47) +- **transit** layer: Add `network`, `ref`, `route`, and `service` properties. [#47](https://github.com/protomaps/basemaps/pull/47) +- **transit** layer: Add `aerialway`, `aeroway`, `highspeed`, `man_made`, and `railway` properties (though don't use them to be v4 safe) [#47](https://github.com/protomaps/basemaps/pull/47) +- **transit** layer: Remove small lines at low- and mid-zooms to improve figure-ground contrast and reduce file size. [#37](https://github.com/protomaps/basemaps/pull/37) +- **water** layer: Use Natural Earth for low-zoom water polygons [#47](https://github.com/protomaps/basemaps/pull/47) +- **water** layer: Use better `kind` values to match new label points in physical points layer [#47](https://github.com/protomaps/basemaps/pull/47) +- **water** layer: Add tunnel / bridge indicator with `pmap:level` (same as roads layer in v2) and `alkaline`, `intermittent`, and `reservoir` boolean indicators [#47](https://github.com/protomaps/basemaps/pull/47) +- **water** layer: Reduce small polygons to improve figure-ground contrast and reduce file size. [#47](https://github.com/protomaps/basemaps/pull/47) + +#### BUG FIXES + +- See also the "Breaking Changes" section above... +- **buildings** layer: Drop all names from buildings. [#47](https://github.com/protomaps/basemaps/pull/47) +- **buildings** layer: Exclude "no" features from `building` and `building_part`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **places** layer: Fix parsing of OSM `population` values to be comma safe. [#38](https://github.com/protomaps/basemaps/pull/38) +- **places** layer: Fix parsing of OSM `population` values to be null safe. [#22](https://github.com/protomaps/basemaps/pull/22) +- **roads** layer: Remove OSM highway features that have been "abandoned", "razed", "demolished", or "removed". [#35](https://github.com/protomaps/basemaps/pull/35) +- **transit** layer: Remove OSM transit features that have been "razed", "demolished", "removed", or "proposed". [#35](https://github.com/protomaps/basemaps/pull/35) +- Fix project path in README.md [#24](https://github.com/protomaps/basemaps/pull/40) +- Fix Markdown in link formatting [#24](https://github.com/protomaps/basemaps/pull/24) + +#### INTERNAL CHANGES + +- Update default Protomaps style in MapLibre JS so it shows off the new features (and any impactful changes). [#37](https://github.com/protomaps/basemaps/pull/37) and [#47](https://github.com/protomaps/basemaps/pull/47) +- Update default Protomaps style in MapLibre JS so road casing layers are below other road layers [#42](https://github.com/protomaps/basemaps/pull/42) +- Update default Protomaps style in MapLibre JS so country boundary lines are solid [#45](https://github.com/protomaps/basemaps/pull/45) +- Update default Protomaps style in MapLibre JS with new "basic" color theme [#46](https://github.com/protomaps/basemaps/pull/46) +- Update OpenLayers plugin example to use official olpmtiles library instead of custom library [#49](https://github.com/protomaps/basemaps/pull/49) +- **landuse** layer: Allow fallback coallese of allow-listed OSM tags into `pmap:kind`. [#47](https://github.com/protomaps/basemaps/pull/47) +- **roads** layer: Significant refactor of the kind and other property logic. +- Add Usage section to README: [#20](https://github.com/protomaps/basemaps/pull/20) +- Update Usage section to README for building tiles and applying code linting (formatting): [#37](https://github.com/protomaps/basemaps/pull/37) +- Add Makefile with common build commands for easier development [#47](https://github.com/protomaps/basemaps/pull/47) +- Add ability to build GeoFabrik named `area` regions (eg "monaco") in the CLI. [#20](https://github.com/protomaps/basemaps/pull/20) +- Improve consistency of internal private variable names in Planetiler profile [#47](https://github.com/protomaps/basemaps/pull/47) +- Add SonarCloud linting... [#31](https://github.com/protomaps/basemaps/pull/31) +- Add Spotless [#25](https://github.com/protomaps/basemaps/pull/25) +- Don't version track target, YARN, DStore, or PMTiles artifacts: [#20](https://github.com/protomaps/basemaps/pull/20) + + +v2.1.0 +------ +- **Release date:** 2023-04-26. +- **Credits:** [@bdon](https://github.com/bdon) + +- Initial open source release as a reimplementation of the Protomaps Basemap vector tile schema as a [Planetiler](https://github.com/onthegomap/planetiler/) schema in Java. +- Add openlayers basemap example +- Publish new PMTiles artifact: + - https://r2-public.protomaps.com/protomaps-sample-datasets/protomaps-basemap-opensource-20230408.pmtiles + + +v2.0.0 +------ +- **Release date:** 2020-10-26 +- **Credits:** [@bdon](https://github.com/bdon) + +- Last closed source version of the Protomaps Basemap vector tile schema +- Add build script + + +v1.0.0 +------ +- **Release date:** 2020-03-05. +- **Credits:** [@bdon](https://github.com/bdon) + +- Initiial closed source version of the Protomaps Basemap vector tile schema + + +NOTE: Release numbers follow [Semantic Versioning](SEMANTIC-VERSIONING.md). See also current project [VERSION](VERSION), the release notes here are for tagged releases; pre-release development changes are often not summarized until a tagged release. \ No newline at end of file diff --git a/SEMANTIC-VERSIONING.md b/SEMANTIC-VERSIONING.md new file mode 100644 index 00000000..f686639d --- /dev/null +++ b/SEMANTIC-VERSIONING.md @@ -0,0 +1,200 @@ +# Semantic Versioning + +When a new version of the Protomaps basemap schema is released, developers should be able to tell from the +version increment how much effort it will take them to integrate the new tiles with their map. We use semantic +versioning to communicate this. + +### What is Semantic Versioning? + +Semantic versioning (or [SemVer](http://semver.org/)) is a formalized way of making promises with an X.Y.Z version indicator. + +#### Version components + +- `MAJOR`.`MINOR`.`PATCH`, example: `1.0.0` (default) +- `MAJOR`.`MINOR`.`PATCH`-`POSTFIX`, example: `1.0.0-pre1` (optional) + +#### Version parts: + +1. **MAJOR** version **X** for incompatible API changes. +2. **MINOR** version **Y** when adding functionality in a backwards-compatible manner, and +3. **PATCH** version **Z** when fixing backwards-compatible bugs +4. **pre-release** version **-POSTFIX** when releasing developer previews + +**NOTE:** The above applies _after_ software has reached as version `1.0.0`, no promises are made for earlier versions. + +#### Developer level of effort: + +- Major version X: **high** – significant integration challenges, read the changelog closely +- Minor version Y: **low** – some integration challenges, read the changelog +- Bug fixes Z: **none** – simply use the new tiles, skim or ignore the changelog +- Pre-release POSTFIX: **low to high** – some to significant integration challenges, read the changelog + +## Versioning the Protomaps Basemap vector tile schema + +Upon our version `1.0.0` release Protomaps Basemap makes the following promises based on the Tilezen schema: + +##### Definition of terms + +* **`common`** - These `layer`s, `property`s, and `kind`s are generally available across all features in a Tilezen response. + - Establishes basic selection of features and their arrangement into specific named layers. + - Core properties needed for display and labeling of features: + - Special bits that make vector tile content **interoperably Tilezen**, including `kind`, `kind_detail`, `landuse_kind`, `kind_tile_rank`, `min_zoom`, `max_zoom`, `is_landuse_aoi`, `sort_rank`, `boundary`, and `maritime_boundary`. + - Fundamental properties like `name` (including localized names), `id`, and `source` included on most every feature. +* **`common-optional`** - These are meant to be part of a common set, but may not be present because they aren't relevant or because we don't have the data (primarily feature `property`s, but could also be `layer`s). + - Used to refine feature selection. + - Lightly transformed **interoperable Tilezen** properties based on original data values. Examples include: `country_capital`, `region_capital`, `bicycle_network`, `is_bridge`, `is_link`, `is_tunnel`, `is_bicycle_related`, `is_bus_route`, `walking_network`, `area`, left & right names and localized `name:*` values on lines, and left & right `id` values on lines. + - Fundamental properties like `ref`, `colour`, `population`, `elevation`, `cuisine`, `operator`, `protect_class`, and `sport`. +* **`optional`** - These are the properties of a specific, less important `kind`, or generally present across `kind`s but only in exceptional cases. + - Often used to decorate features already selected for display. + - Additional properties like `capacity` and `covered`. + +#### MAJOR version increments: + +1. **Remove** `common layer` +1. **Change** `common layer` **name** +1. **Remove** `common-optional layer` +1. **Change** `common-optional layer` **name** +1. **Remove** `common` feature **property** completely from all zooms +1. **Change** `common` feature **property name** +1. **Remove** `common-optional` feature **property** at zoom 14 or more +1. **Change** `common-optional` feature **property name** +1. **Remove** `optional` feature **property** at zoom 14 or more +1. **Change** `kind` **value name** +1. **Remove** `kind` **value** completely from all zooms +1. **Move** `kind` from one layer to another +1. **Additional simplification** across `kind` **values** in zooms 14, 15, or 16 (or max zoom) by removing `common`, `common-optional`, and/or `optional` **properties** by merging or other method +1. **Simplification** within `kind` **values** at zooms 16 (or max zoom) by removing `common`, `common-optional`, and `optional` **properties** by merging or other method +1. **Change** of <= -3 (earlier) to default `min_zoom` or `max_zoom` **values** to determine when `kind` is included +1. **Change** of >= +2 (later) to default `min_zoom` or `max_zoom` **values** to determine when `kind` is included + +#### MINOR version increments: + +1. **Add** `common` layer +1. **Add** `common-optional` layer +1. **Add** `common` feature **property** +1. **Add** `common-optional` feature **property** +1. **Add** `optional` feature **property** +1. **Add** new `kind` value +1. **Additional simplification** across `kind` **values** at zooms 13 or less by removing `common`, `common-optional`, and `optional` **properties** by merging or other method +1. **Additional simplification** within `kind` **values** at zooms 13, 14, or 15 by removing `common`, `common-optional`, and `optional` **properties** by merging or other method +1. **Reassign** 50% or more of existing `kind` **value** into a new `kind` **value**, when kind has 10,000 or more features +1. **Change** of <= -2 (earlier) to `min_zoom` or `max_zoom` **values** to determine when `kind` is included +1. **Change** of >= +1 (later) to default `min_zoom` or `max_zoom` **values** to determine when `kind` is included +1. **Change** the maximum Tilezen zoom (currently zoom 16). + +#### PATCH version increments + +1. **Add** `optional` layer +1. **Change** `optional` layer **name** +1. **Remove** `optional` layer +1. **Additional simplification** within `kind` **values** at zooms 12 or less by removing `common`, `common-optional`, and `optional` **properties** by merging or other method +1. **Reassign** an existing `kind` **value** to an another existing `kind` value when they are equivalent (e.g. fixing the spelling of a value coming from an upstream data source) +1. **Reassign** less than 50% of existing `kind` **value** into a new `kind` value, when kind has 10,000 or more features +1. **Reassign** more than 50% of existing `kind` **value** into a new `kind` value, when kind has less than 10,000 features +1. **Change** of -1 to default `min_zoom` or `max_zoom` **values** to determine when `kind` is included +1. **Adjustments** to the overall map balance (proportion of features in one layer or another, proportion of `kind`s in a single layer) by adjusting boosting of `min_zoom` values over the `kind`'s default, limiting the number of individual `kind` features in a given tile coordinate, and other means. +1. **Add** unpublicized `kind` **value** +1. **Remove** unpublicized `kind` **value** +1. **Correct** a regression in the API (to the last good version) +1. **Correct** a newly added feature **property name** +1. **Correct** a newly added `kind` **value** + +## Versioning Data + +We do not version data features, but we do attempt to indicate the data source and the feature ID as assigned by that source so customers can investigate upstream changes. + + +### Frequency of data updates + +Tilezen has 4 primary sources: + +- **Natural Earth** (used for zooms 0-8 for most everything) updates infrequently (often annually) +- **OpenStreetMap** (used for zooms 9+ for most everything, sometimes earlier) updates frequently (at least daily) +- **OpenStreetMapData** (used for zooms 9+ in the earth and water layers only) updates infrequently (optimistically monthly) +- **Who’s On First** (used for zooms 12+ for places layer) updates frequently (at least daily) + + +### INDIVIDUAL FEATURES are not versioned + +#### Examples: + +1. **Add** new feature +1. **Removal** of existing feature +1. **Change** feature **name** +1. **Change** feature **geometry** +1. **Change** feature **property** values (including property removal if removed from original source) +1. **Change** feature `kind` **value** (when upstream data source reclassifies them). +1. **Change** feature `min_zoom` &/or `max_zoom` **values** (when area or other signal changes upstream). + +**NOTE:** It is possible to query the version of individual features by looking at a feature's `source` and `id` properties and performing a lookup via the source service, but that is beyond the scope of Tilezen. Because of simplification, `id` properties are not always available due to feature merging. + +### LANGUAGES are not versioned + +In addition to the `common` **name** locals call a place, the following `common` and `common-optional` languages are generally available: + +#### Common languages: + +1. `name:ar` **العربية (Arabic)** _common_ +1. `name:bn` **বাংলা (Bengali)** – _common-optional_ +1. `name:de` **Deutsch (German)** – _common-optional_ +1. `name:en` **English** – _common_ +1. `name:es` **español (Spanish)** – _common_ +1. `name:fr` **français (French)** – _common_ +1. `name:el` **ελληνικά (Greek)** – _common-optional_ +1. `name:hi` **हिन्दी (Hindi)** – _common-optional_ +1. `name:id` **Bahasa Indonesia (Indonesian)** – _common-optional_ +1. `name:it` **italiano (Italian)** – _common-optional_ +1. `name:ja` **日本語 (Japanese)** – _common-optional_ +1. `name:ko` **한국어 (Korean)** – _common-optional_ +1. `name:nl` **Nederlands (Dutch)** – _common-optional_ +1. `name:pl` **Polski (Polish)** – _common-optional_ +1. `name:pt` **Português (Portuguese)** – _common-optional_ +1. `name:ru` **Русский (Russian)** – _common_ +1. `name:sv` **Svenska (Swedish)** – _common-optional_ +1. `name:tr` **Türkçe (Turkish)** – _common-optional_ +1. `name:vi` **Tiếng Việt (Vietnamese)** – _common-optional_ +1. `name:zh` **中文 (Chinese)**: primarily simplified but sometimes traditional – _common_ – _deprecated_ +1. `name:zh-Hans` **中文 (Chinese)**: primarily simplified but sometimes traditional – _common_ +1. `name:zh-Hant` **中文 (Chinese)**: primarily traditional but sometimes simplified – _common_ + +Arabic, Chinese, English, French, Russian and Spanish are used by the United Nations for meetings and official documents. The other languages listed are either proposed as official language of the United Nations (Bengali, Hindi, Portugese, and Turkish) or frequently used in OpenStreetMap, Who's On First, or Wikipedia. + +Additional localized names are available as `common-optional` and `optional`, but their actual use is the data is not widespread. + + +### POLITICAL GEOGRAPHY is not versioned + +#### Examples: + +1. **Major** additions, deletions to country names, borders, disputed territories, and capitals are possible and may be advertised but do not bump the Tilezen API version. +1. **Minor** corrections to country names, borders, disputed territories, capitals, and other administrative geography are always possible and will not be tracked or advertised. + +## Versioning the Tilezen Service + +#### MAJOR version increments: + +1. **Rollup** of MAJOR Tilezen API changes +1. **Change** the maximum Tilezen zoom at which tiles are generated by default (currently zoom 16) +1. **Remove** a file format +1. Backwards incompatible change to a file format +1. Tilezen software dependency has a breaking change to the tile response + +#### MINOR version increments: + +1. **Rollup** of MINOR Tilezen API changes +1. **Add** a file format + +#### PATCH version increments: + +1. **Rollup** of PATCH Tilezen API changes +1. **Infrequent update** of static data sources like Natural Earth or OpenStreetMapData +1. Backwards compatible change to a file format +1. Software dependency has a backwards compatible change or bug fix with no change to the tile response + +## See also + +- [VERSION](VERSION) +- http://semver.org +- https://github.com/tilezen/vector-datasource/edit/master/docs/SEMANTIC-VERSIONING.md +- https://github.com/nvkelso/natural-earth-vector/blob/master/README.md +- https://github.com/whosonfirst/whosonfirst-placetypes#roles diff --git a/VERSION b/VERSION new file mode 100644 index 00000000..690a6220 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v3.0.0-pre1 \ No newline at end of file diff --git a/base/src/base_layers.ts b/base/src/base_layers.ts index 58862ead..998e600f 100644 --- a/base/src/base_layers.ts +++ b/base/src/base_layers.ts @@ -26,8 +26,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { "source-layer": "landuse", filter: [ "any", - ["==", "pmap:kind", "park"], - ["==", "landuse", "cemetery"], + ["in", "pmap:kind", "national_park", "park", "cemetery", "protected_area", "nature_reserve", "forest", "golf_course"], ], paint: { "fill-color": c.park, @@ -58,11 +57,41 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "landuse", - filter: ["any", ["==", "pmap:kind", "school"]], + filter: ["any", ["in", "pmap:kind", "school", "university", "college"]], paint: { "fill-color": c.school, }, }, + { + id: "landuse_beach", + type: "fill", + source: source, + "source-layer": "landuse", + filter: ["any", ["in", "pmap:kind", "beach"]], + paint: { + "fill-color": c.sand, + }, + }, + { + id: "landuse_zoo", + type: "fill", + source: source, + "source-layer": "landuse", + filter: ["any", ["in", "pmap:kind", "zoo"]], + paint: { + "fill-color": c.zoo, + }, + }, + { + id: "landuse_military", + type: "fill", + source: source, + "source-layer": "landuse", + filter: ["any", ["in", "pmap:kind", "military", "naval_base", "airfield"]], + paint: { + "fill-color": c.zoo, + }, + }, { id: "natural_wood", type: "fill", @@ -70,9 +99,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { "source-layer": "natural", filter: [ "any", - ["==", "natural", "wood"], - ["==", "leisure", "nature_reserve"], - ["==", "landuse", "forest"], + ["in", "pmap:kind", "wood", "nature_reserve", "forest"], ], paint: { "fill-color": c.wood, @@ -83,7 +110,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "landuse", - filter: ["any", ["==", "highway", "footway"]], + filter: ["any", ["==", "pmap:kind", "pedestrian"]], paint: { "fill-color": c.pedestrian, }, @@ -93,7 +120,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "natural", - filter: ["in", "natural", "scrub", "grassland"], + filter: ["in", "pmap:kind", "scrub", "grassland", "grass"], paint: { "fill-color": c.scrub, }, @@ -103,7 +130,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "natural", - filter: ["==", "natural", "glacier"], + filter: ["==", "pmap:kind", "glacier"], paint: { "fill-color": c.glacier, }, @@ -113,7 +140,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "natural", - filter: ["==", "natural", "sand"], + filter: ["==", "pmap:kind", "sand"], paint: { "fill-color": c.sand, }, @@ -123,7 +150,10 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "landuse", - filter: ["==", "aeroway", "aerodrome"], + filter: [ + "any", + ["in", "pmap:kind", "aerodrome"], + ], paint: { "fill-color": c.aerodrome, }, @@ -133,10 +163,48 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "transit", - filter: ["has", "aeroway"], + filter: [ + "any", + ["in", "pmap:kind_detail", "runway"], + ], + paint: { + "line-color": c.runway, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 10, + 0, + 10.5, + 2, + 15, + 6, + ], + }, + }, + { + id: "transit_taxiway", + type: "line", + source: source, + "source-layer": "transit", + min_zoom: 13, + filter: [ + "any", + ["in", "pmap:kind_detail", "taxiway"], + ], paint: { "line-color": c.runway, - "line-width": 6, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 13, + 0, + 13.5, + 1, + 15, + 6, + ], }, }, { @@ -144,11 +212,10 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "fill", source: source, "source-layer": "landuse", + min_zoom: 14, filter: [ "any", - ["==", "aeroway", "runway"], - ["==", "area:aeroway", "runway"], - ["==", "area:aeroway", "taxiway"], + ["in", "pmap:kind_detail", "runway", "taxiway"], ], paint: { "fill-color": c.runway, @@ -170,7 +237,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { "source-layer": "landuse", filter: [ "any", - ["==", "man_made", "pier"], + ["==", "pmap:kind", "pier"], ], paint: { "fill-color": c.earth, @@ -181,7 +248,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "other"]], + filter: ["all", ["<", "pmap:level", 0], ["in", "pmap:kind", "other", "path"]], paint: { "line-color": c.tunnel_other_casing, "line-gap-width": [ @@ -238,6 +305,44 @@ export function nolabels_layers(source: string, c: Theme): any[] { visibility: casingVisibility, }, }, + { + id: "roads_tunnels_link_casing", + type: "line", + source: source, + "source-layer": "roads", + filter: [ + "all", + ["<", "pmap:level", 0], + ["==", "pmap:link", 1] + ], + paint: { + "line-color": c.tunnel_minor_casing, + "line-dasharray": [3, 2], + "line-gap-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 32, + ], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 1, + ], + }, + layout: { + visibility: casingVisibility, + }, + }, { id: "roads_tunnels_medium_casing", type: "line", @@ -319,7 +424,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], paint: { "line-color": c.tunnel_highway_casing, "line-dasharray": [3, 2], @@ -353,7 +458,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "other"]], + filter: ["all", ["<", "pmap:level", 0], ["in", "pmap:kind", "other", "path"]], paint: { "line-color": c.tunnel_other, "line-dasharray": [1, 1], @@ -395,6 +500,31 @@ export function nolabels_layers(source: string, c: Theme): any[] { ], }, }, + { + id: "roads_tunnels_link", + type: "line", + source: source, + "source-layer": "roads", + filter: [ + "all", + ["<", "pmap:level", 0], + ["==", "pmap:link", 1], + ], + paint: { + "line-color": c.tunnel_minor, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 20, + 32, + ], + }, + }, { id: "roads_tunnels_medium", type: "line", @@ -450,7 +580,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + filter: ["all", ["<", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], paint: { "line-color": c.tunnel_highway, "line-width": [ @@ -467,16 +597,39 @@ export function nolabels_layers(source: string, c: Theme): any[] { }, }, { - id: "physical_line_waterway", + id: "physical_line_stream", type: "line", source: source, "source-layer": "physical_line", - filter: ["==", ["get", "pmap:kind"], "waterway"], + min_zoom: 14, + filter: ["all", ["in", "pmap:kind", "stream"]], paint: { "line-color": c.water, "line-width": 0.5, }, }, + { + id: "physical_line_river", + type: "line", + source: source, + "source-layer": "physical_line", + min_zoom: 9, + filter: ["all", ["in", "pmap:kind", "river"]], + paint: { + "line-color": c.water, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 9, + 0, + 9.5, + 1.0, + 18, + 12, + ], + }, + }, { id: "buildings", type: "fill-extrusion", @@ -488,6 +641,69 @@ export function nolabels_layers(source: string, c: Theme): any[] { "fill-extrusion-opacity": 0.5, }, }, + { + id: "transit_pier", + type: "line", + source: source, + "source-layer": "transit", + filter: [ + "any", + ["==", "pmap:kind", "pier"], + ], + paint: { + "line-color": c.minor, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 0.5, + 20, + 16, + ], + }, + }, + { + id: "roads_minor_service_casing", + type: "line", + source: source, + "source-layer": "roads", + minzoom: 13, + filter: [ + "all", + ["==", "pmap:level", 0], + ["==", "pmap:kind", "minor_road"], + ["==", "pmap:kind_detail", "service"], + ], + paint: { + "line-color": c.minor_casing, + "line-gap-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 13, + 0, + 13.5, + 0.25, + 20, + 24, + ], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 13, + 0, + 13.5, + 0.8, + ], + }, + layout: { + visibility: casingVisibility, + }, + }, { id: "roads_minor_casing", type: "line", @@ -497,6 +713,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { "all", ["==", "pmap:level", 0], ["==", "pmap:kind", "minor_road"], + ["!=", "pmap:kind_detail", "service"], ], paint: { "line-color": c.minor_casing, @@ -509,7 +726,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { 12.5, 0.5, 20, - 32, + 24, ], "line-width": [ "interpolate", @@ -518,7 +735,44 @@ export function nolabels_layers(source: string, c: Theme): any[] { 12, 0, 12.5, - 1, + .6, + ], + }, + layout: { + visibility: casingVisibility, + }, + }, + { + id: "roads_link_casing", + type: "line", + source: source, + "source-layer": "roads", + minzoom: 13, + filter: [ + "all", + ["==", "pmap:link", 1], + ], + paint: { + "line-color": c.minor_casing, + "line-gap-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 13, + 0, + 13.5, + 0.5, + 20, + 24, + ], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 13, + 0, + 13.5, + 2, ], }, layout: { @@ -563,10 +817,11 @@ export function nolabels_layers(source: string, c: Theme): any[] { }, }, { - id: "roads_major_casing", + id: "roads_major_casing_late", type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", ["==", "pmap:level", 0], @@ -600,11 +855,12 @@ export function nolabels_layers(source: string, c: Theme): any[] { }, }, { - id: "roads_highway_casing", + id: "roads_highway_casing_late", type: "line", source: source, "source-layer": "roads", - filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + minzoom: 12, + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], paint: { "line-color": c.highway_casing, "line-gap-width": [ @@ -637,7 +893,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "other"]], + filter: ["all", ["==", "pmap:level", 0], ["in", "pmap:kind", "other", "path"]], paint: { "line-color": c.other, "line-dasharray": [2, 1], @@ -655,13 +911,13 @@ export function nolabels_layers(source: string, c: Theme): any[] { }, }, { - id: "transit_pier", + id: "roads_link", type: "line", source: source, - "source-layer": "transit", + "source-layer": "roads", filter: [ - "any", - ["==", "pmap:kind", "pier"], + "all", + ["==", "pmap:link", 1], ], paint: { "line-color": c.minor, @@ -669,12 +925,38 @@ export function nolabels_layers(source: string, c: Theme): any[] { "interpolate", ["exponential", 1.6], ["zoom"], - 12, + 13, 0, - 12.5, - 0.5, + 13.5, + 1, 20, - 16, + 24, + ], + }, + }, + { + id: "roads_minor_service", + type: "line", + source: source, + "source-layer": "roads", + filter: [ + "all", + ["==", "pmap:level", 0], + ["==", "pmap:kind", "minor_road"], + ["==", "pmap:kind_detail", "service"], + ], + paint: { + "line-color": c.minor, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 13, + 0, + 13.5, + 0.25, + 20, + 24, ], }, }, @@ -687,6 +969,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { "all", ["==", "pmap:level", 0], ["==", "pmap:kind", "minor_road"], + ["!=", "pmap:kind_detail", "service"], ], paint: { "line-color": c.minor, @@ -728,6 +1011,44 @@ export function nolabels_layers(source: string, c: Theme): any[] { ], }, }, + { + id: "roads_major_casing_early", + type: "line", + source: source, + "source-layer": "roads", + maxzoom: 12, + filter: [ + "all", + ["==", "pmap:level", 0], + ["==", "pmap:kind", "major_road"], + ], + paint: { + "line-color": c.major_casing, + "line-gap-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 0.5, + 19, + 32, + ], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 9, + 0, + 9.5, + 1, + ], + }, + layout: { + visibility: casingVisibility, + }, + }, { id: "roads_major", type: "line", @@ -744,21 +1065,55 @@ export function nolabels_layers(source: string, c: Theme): any[] { "interpolate", ["exponential", 1.6], ["zoom"], - 7, + 6, 0, - 7.5, + 6.5, 0.5, 19, 32, ], }, }, + { + id: "roads_highway_casing_early", + type: "line", + source: source, + "source-layer": "roads", + maxzoom: 12, + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], + paint: { + "line-color": c.highway_casing, + "line-gap-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 3, + 0, + 3.5, + 0.5, + 18, + 32, + ], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 7, + 0, + 7.5, + 1, + ], + }, + layout: { + visibility: casingVisibility, + }, + }, { id: "roads_highway", type: "line", source: source, "source-layer": "roads", - filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + filter: ["all", ["==", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], paint: { "line-color": c.highway, "line-width": [ @@ -779,7 +1134,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "transit", - filter: ["all", ["==", "pmap:kind", "railway"]], + filter: ["all", ["==", "pmap:kind", "rail"]], paint: { "line-color": c.railway, "line-width": 2, @@ -790,7 +1145,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "transit", - filter: ["all", ["==", "pmap:kind", "railway"]], + filter: ["all", ["==", "pmap:kind", "rail"]], paint: { "line-color": c.railway_tracks, "line-width": 0.8, @@ -813,7 +1168,8 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "other"]], + minzoom: 12, + filter: ["all", [">", "pmap:level", 0], ["in", "pmap:kind", "other", "path"]], paint: { "line-color": c.bridges_other_casing, "line-gap-width": [ @@ -832,11 +1188,50 @@ export function nolabels_layers(source: string, c: Theme): any[] { visibility: casingVisibility, }, }, + { + id: "roads_bridges_link_casing", + type: "line", + source: source, + "source-layer": "roads", + minzoom: 12, + filter: [ + "all", + [">", "pmap:level", 0], + ["==", "pmap:link", 1], + ], + paint: { + "line-color": c.bridges_minor_casing, + "line-gap-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 1, + 20, + 32, + ], + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 2, + ], + }, + layout: { + visibility: casingVisibility, + }, + }, { id: "roads_bridges_minor_casing", type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", [">", "pmap:level", 0], @@ -874,6 +1269,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", [">", "pmap:level", 0], @@ -911,6 +1307,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", [">", "pmap:level", 0], @@ -948,7 +1345,8 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + minzoom: 12, + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], paint: { "line-color": c.bridges_highway_casing, "line-gap-width": [ @@ -981,7 +1379,8 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "other"]], + minzoom: 12, + filter: ["all", [">", "pmap:level", 0], ["in", "pmap:kind", "other", "path"]], paint: { "line-color": c.bridges_other, "line-dasharray": [2, 1], @@ -1003,6 +1402,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", [">", "pmap:level", 0], @@ -1023,11 +1423,38 @@ export function nolabels_layers(source: string, c: Theme): any[] { ], }, }, + { + id: "roads_bridges_link", + type: "line", + source: source, + "source-layer": "roads", + minzoom: 12, + filter: [ + "all", + [">", "pmap:level", 0], + ["==", "pmap:link", 1], + ], + paint: { + "line-color": c.bridges_minor, + "line-width": [ + "interpolate", + ["exponential", 1.6], + ["zoom"], + 12, + 0, + 12.5, + 1, + 20, + 32, + ], + }, + }, { id: "roads_bridges_medium", type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", [">", "pmap:level", 0], @@ -1053,6 +1480,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", + minzoom: 12, filter: [ "all", [">", "pmap:level", 0], @@ -1078,7 +1506,7 @@ export function nolabels_layers(source: string, c: Theme): any[] { type: "line", source: source, "source-layer": "roads", - filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"]], + filter: ["all", [">", "pmap:level", 0], ["==", "pmap:kind", "highway"], ["!=", "pmap:link", 1]], paint: { "line-color": c.bridges_highway, "line-width": [ @@ -1104,12 +1532,13 @@ export function labels_layers(source: string, c: Theme): any[] { type: "symbol", source: source, "source-layer": "physical_line", - minzoom: 14, + minzoom: 13, + filter: ["all", ["in", "pmap:kind", "river", "stream"]], layout: { "symbol-placement": "line", "text-font": ["NotoSans-Regular"], "text-field": ["get", "name"], - "text-size": 14, + "text-size": 12, "text-letter-spacing": 0.3, }, paint: { @@ -1119,11 +1548,32 @@ export function labels_layers(source: string, c: Theme): any[] { }, }, { - id: "roads_labels", + id: "physical_point_peak", + type: "symbol", + source: source, + "source-layer": "physical_point", + filter: ["any", ["==", "pmap:kind", "peak"]], + layout: { + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 11, + "text-max-width": 9 + }, + paint: { + "text-color": c.peak_label, + "text-halo-color": c.peak_label_halo, + "text-halo-width": 1.5, + }, + }, + { + id: "roads_labels_minor", type: "symbol", source: source, "source-layer": "roads", + minzoom: 15, + filter: ["any", ["in", "pmap:kind", "minor_road", "other", "path"]], layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], "symbol-placement": "line", "text-font": ["NotoSans-Regular"], "text-field": ["get", "name"], @@ -1135,6 +1585,29 @@ export function labels_layers(source: string, c: Theme): any[] { "text-halo-width": 2, }, }, + { + id: "pois_high_zooms", + type: "symbol", + source: source, + minzoom: 15, + "source-layer": "pois", + "filter": ["any", [">=", ["get", "pmap:min_zoom"], 13]], + layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 11, + "text-max-width": 9, + "icon-padding": ["interpolate", ["linear"], + ["zoom"], 0, 2, 14, 2, 16, 20, 17, 2, 22, 2 + ], + }, + paint: { + "text-color": c.subplace_label, + "text-halo-color": c.subplace_label_halo, + "text-halo-width": 1.5, + }, + }, { id: "mask", type: "fill", @@ -1149,12 +1622,13 @@ export function labels_layers(source: string, c: Theme): any[] { type: "symbol", source: source, "source-layer": "physical_point", - filter: ["any", ["==", "place", "sea"], ["==", "place", "ocean"]], + filter: ["any", ["in", "pmap:kind", "sea", "ocean", "lake", "water", "bay", "strait", "fjord"]], layout: { "text-font": ["NotoSans-Regular"], "text-field": ["get", "name"], - "text-size": 13, + "text-size": 11, "text-letter-spacing": 0.1, + "text-max-width": 9 }, paint: { "text-color": c.ocean_label, @@ -1163,37 +1637,23 @@ export function labels_layers(source: string, c: Theme): any[] { }, }, { - id: "physical_point_peak", + id: "roads_labels_major", type: "symbol", source: source, - "source-layer": "physical_point", - filter: ["any", ["==", "natural", "peak"]], - layout: { - "text-font": ["NotoSans-Regular"], - "text-field": ["get", "name"], - "text-size": 14, - }, - paint: { - "text-color": c.peak_label, - "text-halo-color": c.peak_label_halo, - "text-halo-width": 1.5, - }, - }, - { - id: "pois", - type: "symbol", - source: source, - minzoom: 17, - "source-layer": "pois", + "source-layer": "roads", + minzoom: 11, + filter: ["any", ["in", "pmap:kind", "highway", "major_road", "medium_road"]], layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], + "symbol-placement": "line", "text-font": ["NotoSans-Regular"], "text-field": ["get", "name"], "text-size": 12, }, paint: { - "text-color": c.subplace_label, - "text-halo-color": c.subplace_label_halo, - "text-halo-width": 1.5, + "text-color": c.roads_label, + "text-halo-color": c.roads_label_halo, + "text-halo-width": 2, }, }, { @@ -1203,8 +1663,11 @@ export function labels_layers(source: string, c: Theme): any[] { "source-layer": "places", filter: ["==", "pmap:kind", "neighbourhood"], layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], "text-field": "{name}", "text-font": ["NotoSans-Regular"], + "text-max-width": 7, + "text-padding": 4, "text-size": { base: 1.2, stops: [ @@ -1217,15 +1680,37 @@ export function labels_layers(source: string, c: Theme): any[] { paint: { "text-color": c.subplace_label, "text-halo-color": c.subplace_label_halo, - "text-halo-width": 0.5, + "text-halo-width": 1.0, }, }, { - id: "places_city_circle", + id: "pois_important", + type: "symbol", + source: source, + "source-layer": "pois", + "filter": ["any", ["<", ["get", "pmap:min_zoom"], 13]], + layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], + "text-font": ["NotoSans-Regular"], + "text-field": ["get", "name"], + "text-size": 11, + "text-max-width": 9, + "icon-padding": ["interpolate", ["linear"], + ["zoom"], 0, 2, 14, 2, 16, 20, 17, 2, 22, 2 + ], + }, + paint: { + "text-color": c.subplace_label, + "text-halo-color": c.subplace_label_halo, + "text-halo-width": 1.5, + }, + }, + { + id: "places_locality_circle", type: "circle", source: source, "source-layer": "places", - filter: ["==", "pmap:kind", "city"], + filter: ["==", "pmap:kind", "locality"], paint: { "circle-radius": 2, "circle-stroke-width": 2, @@ -1235,16 +1720,30 @@ export function labels_layers(source: string, c: Theme): any[] { maxzoom: 8, }, { - id: "places_city", + id: "places_locality", type: "symbol", source: source, "source-layer": "places", - filter: ["==", "pmap:kind", "city"], + filter: ["==", "pmap:kind", "locality"], layout: { - "symbol-sort-key": ["number", ["get", "pmap:min_zoom"]], + "symbol-sort-key": ["to-number", ["concat", ["get", "pmap:min_zoom"], + ["+", 1, ["/", 1, ["get", "pmap:population_rank"]]] + ]], "text-field": "{name}", - "text-font": ["NotoSans-Bold"], - "text-size": ["step", ["get", "pmap:rank"], 0, 1, 12, 2, 10], + "text-font": ["NotoSans-Regular"], + "text-size": ["interpolate", ["linear"], + ["zoom"], + 4, ["case", ["<", ["get", "pmap:population_rank"], 10], 11, + [">=", ["get", "pmap:population_rank"], 10], 12, 0], + 6.99, ["case", ["<", ["get", "pmap:population_rank"], 9], 11, + [">=", ["get", "pmap:population_rank"], 9], 13, 0], + 15, ["case", ["<", ["get", "pmap:population_rank"], 8], 12, + [">=", ["get", "pmap:population_rank"], 8], 14, 0 + ] + ], + "icon-padding": ["interpolate", ["linear"], + ["zoom"], 0, 2, 8, 4, 10, 8, 12, 6, 22, 2 + ], "text-anchor": { stops: [ [7, "left"], @@ -1260,12 +1759,13 @@ export function labels_layers(source: string, c: Theme): any[] { }, }, { - id: "places_state", + id: "places_region", type: "symbol", source: source, "source-layer": "places", - filter: ["==", "pmap:kind", "state"], + filter: ["==", "pmap:kind", "region"], layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], "text-field": "{name}", "text-font": ["NotoSans-Regular"], "text-size": 12, @@ -1284,18 +1784,24 @@ export function labels_layers(source: string, c: Theme): any[] { type: "symbol", source: source, "source-layer": "places", - filter: ["==", "place", "country"], + filter: ["==", "pmap:kind", "country"], layout: { + "symbol-sort-key": ["get", "pmap:min_zoom"], "text-field": "{name}", "text-font": ["NotoSans-Bold"], - "text-size": { - base: 1.2, - stops: [ - [2, 13], - [6, 13], - [8, 20], - ], - }, + "text-size": ["interpolate", ["linear"], + ["zoom"], + 2, ["case", ["<", ["get", "pmap:population_rank"], 10], 11, + [">=", ["get", "pmap:population_rank"], 10], 12, 0], + 6, ["case", ["<", ["get", "pmap:population_rank"], 8], 11, + [">=", ["get", "pmap:population_rank"], 8], 14, 0], + 8, ["case", ["<", ["get", "pmap:population_rank"], 7], 12, + [">=", ["get", "pmap:population_rank"], 7], 20, 0 + ] + ], + "icon-padding": ["interpolate", ["linear"], + ["zoom"], 0, 2, 14, 2, 16, 20, 17, 2, 22, 2 + ], "text-transform": "uppercase", }, paint: { diff --git a/base/src/colors.ts b/base/src/colors.ts index b56d4042..4dd0622f 100644 --- a/base/src/colors.ts +++ b/base/src/colors.ts @@ -14,6 +14,7 @@ export interface Theme { aerodrome: string; runway: string; water: string; + zoo: string; tunnel_other_casing: string; tunnel_other: string; tunnel_minor_casing: string; @@ -65,6 +66,7 @@ export interface Theme { state_label_halo: string; country_label: string; country_label_halo: string; + debug: string; } export const LIGHT: Theme = { @@ -83,6 +85,7 @@ export const LIGHT: Theme = { aerodrome: "#dbe7e7", runway: "#d1d9d9", water: "#a4cae1", + zoo: "#EBE6ED", tunnel_other_casing: "#ffffff", tunnel_other: "#f7f7f7", tunnel_minor_casing: "#e2e2e2", @@ -134,6 +137,7 @@ export const LIGHT: Theme = { state_label_halo: "White", country_label: "#9590aa", country_label_halo: "white", + debug: "red", }; export const DARK: Theme = { @@ -152,6 +156,7 @@ export const DARK: Theme = { aerodrome: "#000000", runway: "#000000", water: "#1e293b", + zoo: "#EBE6ED", tunnel_other_casing: "#ffffff", tunnel_other: "#000000", tunnel_minor_casing: "#ffffff", @@ -203,6 +208,7 @@ export const DARK: Theme = { state_label_halo: "#000000", country_label: "#ffffff", country_label_halo: "#000000", + debug: "red", }; export const WHITE: Theme = { @@ -221,6 +227,7 @@ export const WHITE: Theme = { aerodrome: "#eee", runway: "#eee", water: "#eeeeee", + zoo: "#EBE6ED", tunnel_other_casing: "#ff00ff", tunnel_other: "#c8c8c8", tunnel_minor_casing: "#ff00ff", @@ -272,6 +279,7 @@ export const WHITE: Theme = { state_label_halo: "#ffffff", country_label: "#bbb", country_label_halo: "#ffffff", + debug: "red", }; export const BLACK: Theme = { @@ -290,6 +298,7 @@ export const BLACK: Theme = { aerodrome: "#060606", runway: "#060606", water: "#333", + zoo: "#EBE6ED", tunnel_other_casing: "#ff00ff", tunnel_other: "#222", tunnel_minor_casing: "#ff00ff", @@ -341,6 +350,7 @@ export const BLACK: Theme = { state_label_halo: "#000000", country_label: "#555", country_label_halo: "#000000", + debug: "red", }; export const GRAYSCALE: Theme = { @@ -359,6 +369,7 @@ export const GRAYSCALE: Theme = { aerodrome: "#e8e8e8", runway: "#e8e8e8", water: "#ddd", + zoo: "#EBE6ED", tunnel_other_casing: "#ff00ff", tunnel_other: "#ffffff", tunnel_minor_casing: "#ff00ff", @@ -410,6 +421,7 @@ export const GRAYSCALE: Theme = { state_label_halo: "#ffffff", country_label: "#888", country_label_halo: "#ffffff", + debug: "red", }; export default { diff --git a/compare/Makefile b/compare/Makefile new file mode 100644 index 00000000..ee4a9e8d --- /dev/null +++ b/compare/Makefile @@ -0,0 +1,4 @@ +serve: + npm run serve + +.PHONY: serve \ No newline at end of file diff --git a/tiles/Makefile b/tiles/Makefile new file mode 100644 index 00000000..77d1492d --- /dev/null +++ b/tiles/Makefile @@ -0,0 +1,88 @@ +# download dependencies and compile the JAR +# generally do this after each code change +clean: + mvn clean package + +# is a build only partially finishes, the PMTiles can be corrupted +clean-pmtiles: + rm -rf *.pmtiles + +# Run linting to apply code formatting for clean PR merges in CI +lint: + mvn spotless:apply + +# This is optimized for local dev serving PMTiles out of directory local to this Makefile +# Don't use this for production (instead set --cors=ORIGIN) +# The default port is: 8080 (use --port to override) +# Assumes go-pmtiles has been installed locally (eg prebuilt go binary download) & the path is added to your shell env +# https://github.com/protomaps/go-pmtiles +# Example tile coord to test with: http://localhost:8080/monaco/12/2133/1495.mvt +serve: + ~/Downloads/pmtiles serve --cors=* . + +# +# Testing areas +# + +# Smallest +# +# Download and generate monaco.pmtiles in the current directory: +monaco: + java -jar target/*-with-deps.jar --download --force --area=monaco + +# Download and generate us/california.pmtiles in the current directory, clipped to the SF bay area +# Note: the slash confuses some downstream processes, try a symlink to california +sf: + java -jar target/*-with-deps.jar --download --force --area=us/california --bounds=-122.9913,37.2377,-121.5265,38.2519 + +# Medium +# +# Download and generate switzerland.pmtiles in the current directory: +switzerland: + java -jar target/*-with-deps.jar --download --force --area=switzerland + +# Download and generate switzerland.pmtiles in the current directory: +washington: + java -jar target/*-with-deps.jar --download --force --area=washington + +# Large +# +# Download and generate us/california.pmtiles in the current directory: +# Note: the slash confuses some downstream processes, try a symlink to california +california: + java -jar target/*-with-deps.jar --download --force --area=us/california + +# Download and generate us/new-york.pmtiles in the current directory: +# Note: the slash confuses some downstream processes, try a symlink to new-york +ny: + java -jar target/*-with-deps.jar --download --force --area=us/new-york + +# Download and generate us/new-york.pmtiles in the current directory, +# clipped to the NYC metro area (including New Jersey) +# Note: the slash confuses some downstream processes, try a symlink to northeast +nyc: + java -jar target/*-with-deps.jar --download --force --area=us/northeast --bounds=-75.896703,39.530403,-73.260767,41.217394 + +# Download and generate japan.pmtiles in the current directory: +japan: + java -jar target/*-with-deps.jar --download --force --area=japan --maxzoom=10 --render_maxzoom=10 + +us-lowzoom: + java -jar target/*-with-deps.jar --download --force --area=us/california --bounds=-129.525228,18.870954,-59.212729,50.664092 --maxzoom=7 --render_maxzoom=7 + +eu-lowzoom: + java -jar target/*-with-deps.jar --download --force --area=us/california --bounds=-32.318197,31.705746,53.287269,72.937699 --maxzoom=7 --render_maxzoom=7 + +planet: + java -Xmx24g \ + -jar target/*-with-deps.jar \ + `# Download the latest planet.osm.pbf from s3://osm-pds bucket` \ + --area=planet --bounds=planet --download \ + `# Accelerate the download by fetching the 10 1GB chunks at a time in parallel` \ + --download-threads=10 --download-chunk-size-mb=1000 \ + `# Also download name translations from wikidata` \ + --fetch-wikidata \ + --output=output.\pmtiles \ + --nodemap-type=sparsearray --nodemap-storage=ram 2>&1 | tee logs.txt + +.PHONY: clean clean-pmtiles lint serve monaco sf switzerland washington california ny ny-metro japan us-lowzoom eu-lowzoom planet diff --git a/tiles/src/main/java/com/protomaps/basemap/Basemap.java b/tiles/src/main/java/com/protomaps/basemap/Basemap.java index aae2236d..c1757e47 100644 --- a/tiles/src/main/java/com/protomaps/basemap/Basemap.java +++ b/tiles/src/main/java/com/protomaps/basemap/Basemap.java @@ -30,6 +30,7 @@ public Basemap(Boolean useTilezen) { var admin = new Boundaries(); registerHandler(admin); registerSourceHandler("osm", admin); + registerSourceHandler("ne", admin::processNe); var buildings = new Buildings(); registerHandler(buildings); @@ -50,6 +51,7 @@ public Basemap(Boolean useTilezen) { var physical_point = new PhysicalPoint(); registerHandler(physical_point); registerSourceHandler("osm", physical_point); + registerSourceHandler("ne", physical_point::processNe); var place = new Places(); registerHandler(place); diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/CountryNameZooms.java b/tiles/src/main/java/com/protomaps/basemap/feature/CountryNameZooms.java new file mode 100644 index 00000000..ecd27fe3 --- /dev/null +++ b/tiles/src/main/java/com/protomaps/basemap/feature/CountryNameZooms.java @@ -0,0 +1,1216 @@ +package com.protomaps.basemap.feature; + +import com.onthegomap.planetiler.reader.SourceFeature; + +public class CountryNameZooms { + + public static float[] getMinMaxZooms(SourceFeature sf) { + float min_zoom = 8.0f; // default for unrecognized countries + float max_zoom = 11.0f; // default for all countries + + try { + var name = sf.getString("name:en") == null ? sf.getString("name") : sf.getString("name:en"); + + if (name != null) { + switch (name) { + case "Andorra" -> { + // AD country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "United Arab Emirates" -> { + // AE country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Afghanistan" -> { + // AF country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Antigua and Barb" -> { + // AG country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Anguilla" -> { + // AI country code + min_zoom = 11.5f; + max_zoom = 11.5f; + } + case "Albania" -> { + // AL country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Armenia" -> { + // AM country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Angola" -> { + // AO country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Antarctica" -> { + // AQ country code + min_zoom = 14.5f; + max_zoom = 14.5f; + } + case "Argentina" -> { + // AR country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "American Samoa" -> { + // AS country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Austria" -> { + // AT country code + min_zoom = 7.8f; + max_zoom = 11.0f; + } + case "Australia" -> { + // AU country code + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Aruba" -> { + // AW country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Aland" -> { + // AX country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Azerbaijan" -> { + // AZ country code + min_zoom = 9.2f; + max_zoom = 11.0f; + } + case "Bosnia and Herz" -> { + // BA country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Barbados" -> { + // BB country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Bangladesh" -> { + // BD country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Belgium" -> { + // BE country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Burkina Faso" -> { + // BF country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Bulgaria" -> { + // BG country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Bahrain" -> { + // BH country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Burundi" -> { + // BI country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Benin" -> { + // BJ country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "St-Barthelemy" -> { + // BL country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Bermuda" -> { + // BM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Brunei" -> { + // BN country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Bolivia" -> { + // BO country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Brazil" -> { + // BR country code + min_zoom = 3.7f; + max_zoom = 8.5f; + } + case "Bahamas" -> { + // BS country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Bhutan" -> { + // BT country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Botswana" -> { + // BW country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "Belarus" -> { + // BY country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Belize" -> { + // BZ country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Canada" -> { + // CA country code + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Democratic Republic of the Congo" -> { + // CD country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "Central African Republic" -> { + // CF country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Republic of the Congo" -> { + // CG country code + min_zoom = 7.6f; + max_zoom = 11.0f; + } + case "Switzerland" -> { + // CH country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + // Côte d'Ivoire + case "Ivory Coast" -> { + // CI country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Cook Islands" -> { + // CK country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Chile" -> { + // CL country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "Cameroon" -> { + // CM country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "China" -> { + // CN country code + min_zoom = 5.0f; + max_zoom = 10.3f; + } + case "Colombia" -> { + // CO country code + min_zoom = 7.0f; + max_zoom = 11.2f; + } + case "Costa Rica" -> { + // CR country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Cuba" -> { + // CU country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Cabo Verde" -> { + // CV country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + // Curaçao + case "Curacao" -> { + // CW country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Cyprus" -> { + // CY country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Czechia" -> { + // CZ country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Germany" -> { + // DE country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Djibouti" -> { + // DJ country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Denmark" -> { + // DK country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Dominica" -> { + // DM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Dominican Rep" -> { + // DO country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Algeria" -> { + // DZ country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Ecuador" -> { + // EC country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Estonia" -> { + // EE country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Egypt" -> { + // EG country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Western Sahara" -> { + // EH country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Eritrea" -> { + // ER country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Spain" -> { + // ES country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Ethiopia" -> { + // ET country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Finland" -> { + // FI country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Fiji" -> { + // FJ country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Falkland Islands" -> { + // FK country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Micronesia" -> { + // FM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Faeroe Islands" -> { + // FO country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Gabon" -> { + // GA country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "United Kingdom" -> { + // GB country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Grenada" -> { + // GD country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Georgia" -> { + // GE country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Guernsey" -> { + // GG country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Ghana" -> { + // GH country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Gibraltar" -> { + // GI country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Greenland" -> { + // GL country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Gambia" -> { + // GM country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Guinea" -> { + // GN country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Equatorial Guinea" -> { + // GQ country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Greece" -> { + // GR country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "South Georgia and the Islands" -> { + // GS country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Guatemala" -> { + // GT country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Guam" -> { + // GU country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Guinea-Bissau" -> { + // GW country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Guyana" -> { + // GY country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Hong Kong" -> { + // HK country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Heard Island and McDonald Islands" -> { + // HM country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Honduras" -> { + // HN country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Croatia" -> { + // HR country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Haiti" -> { + // HT country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Hungary" -> { + // HU country code + min_zoom = 8.5f; + max_zoom = 11.0f; + } + case "Indonesia" -> { + // ID country code + min_zoom = 5.0f; + max_zoom = 10.1f; + } + case "Ireland" -> { + // IE country code + min_zoom = 8.2f; + max_zoom = 11.0f; + } + case "Israel" -> { + // IL country code + min_zoom = 8.4f; + max_zoom = 11.0f; + } + case "Isle of Man" -> { + // IM country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "India" -> { + // IN country code + min_zoom = 4.6f; + max_zoom = 10.1f; + } + case "British Indian Ocean Territory" -> { + // IO country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Iraq" -> { + // IQ country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Iran" -> { + // IR country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Iceland" -> { + // IS country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "Italy" -> { + // IT country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Jersey" -> { + // JE country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Jamaica" -> { + // JM country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Jordan" -> { + // JO country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Japan" -> { + // JP country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Kenya" -> { + // KE country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Kyrgyzstan" -> { + // KG country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Cambodia" -> { + // KH country code + min_zoom = 8.1f; + max_zoom = 11.0f; + } + case "Kiribati" -> { + // KI country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Comoros" -> { + // KM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "St. Kitts and Nevis" -> { + // KN country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "North Korea" -> { + // KP country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "South Korea" -> { + // KR country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Kuwait" -> { + // KW country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Cayman Islands" -> { + // KY country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Kazakhstan" -> { + // KZ country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "Laos" -> { + // LA country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Lebanon" -> { + // LB country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Saint Lucia" -> { + // LC country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Liechtenstein" -> { + // LI country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Sri Lanka" -> { + // LK country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Liberia" -> { + // LR country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Lesotho" -> { + // LS country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Lithuania" -> { + // LT country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Luxembourg" -> { + // LU country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Latvia" -> { + // LV country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Libya" -> { + // LY country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Morocco" -> { + // MA country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Monaco" -> { + // MC country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Moldova" -> { + // MD country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Montenegro" -> { + // ME country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "St-Martin" -> { + // MF country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Madagascar" -> { + // MG country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Marshall Islands" -> { + // MH country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Macedonia" -> { + // MK country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Mali" -> { + // ML country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Myanmar" -> { + // MM country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Mongolia" -> { + // MN country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "Macao" -> { + // MO country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Northern Mariana Islands" -> { + // MP country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Mauritania" -> { + // MR country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Montserrat" -> { + // MS country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Malta" -> { + // MT country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Mauritius" -> { + // MU country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Maldives" -> { + // MV country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Malawi" -> { + // MW country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Mexico" -> { + // MX country code + min_zoom = 6.9f; + max_zoom = 11.2f; + } + case "Malaysia" -> { + // MY country code + min_zoom = 7.2f; + max_zoom = 11.0f; + } + case "Mozambique" -> { + // MZ country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Namibia" -> { + // NA country code + min_zoom = 6.0f; + max_zoom = 11.0f; + } + case "New Caledonia" -> { + // NC country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Niger" -> { + // NE country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Norfolk Island" -> { + // NF country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Nigeria" -> { + // NG country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Nicaragua" -> { + // NI country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Netherlands" -> { + // NL country code + min_zoom = 8.6f; + max_zoom = 11.0f; + } + case "Nepal" -> { + // NP country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Nauru" -> { + // NR country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Niue" -> { + // NU country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "New Zealand" -> { + // NZ country code + min_zoom = 8.5f; + max_zoom = 11.3f; + } + case "Oman" -> { + // OM country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Panama" -> { + // PA country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Peru" -> { + // PE country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "French Polynesia" -> { + // PF country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Papua New Guinea" -> { + // PG country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Philippines" -> { + // PH country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Pakistan" -> { + // PK country code + min_zoom = 5.0f; + max_zoom = 10.5f; + } + case "Poland" -> { + // PL country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Saint Pierre and Miquelon" -> { + // PM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Pitcairn Islands" -> { + // PN country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Puerto Rico" -> { + // PR country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Palestine" -> { + // PS country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Portugal" -> { + // PT country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Palau" -> { + // PW country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Paraguay" -> { + // PY country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Qatar" -> { + // QA country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Romania" -> { + // RO country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Serbia" -> { + // RS country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Russia" -> { + // RU country code + min_zoom = 5.0f; + max_zoom = 10.2f; + } + case "Rwanda" -> { + // RW country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Saudi Arabia" -> { + // SA country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Solomon Islands" -> { + // SB country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Seychelles" -> { + // SC country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Sudan" -> { + // SD country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Sweden" -> { + // SE country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Singapore" -> { + // SG country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Saint Helena" -> { + // SH country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Slovenia" -> { + // SI country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Slovakia" -> { + // SK country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Sierra Leone" -> { + // SL country code + min_zoom = 7.8f; + max_zoom = 11.0f; + } + case "San Marino" -> { + // SM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Senegal" -> { + // SN country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Somalia" -> { + // SO country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Suriname" -> { + // SR country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "South Sudan" -> { + // SS country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + // São Tomé and Principe + case "Sao Tome and Principe" -> { + // ST country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "El Salvador" -> { + // SV country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Sint Maarten" -> { + // SX country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Syria" -> { + // SY country code + min_zoom = 7.7f; + max_zoom = 11.5f; + } + case "eSwatini" -> { + // SZ country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Turks and Caicos Islands" -> { + // TC country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Chad" -> { + // TD country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "French Southern Antarctic Lands" -> { + // TF country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Togo" -> { + // TG country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Thailand" -> { + // TH country code + min_zoom = 8.2f; + max_zoom = 11.0f; + } + case "Tajikistan" -> { + // TJ country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Timor-Leste" -> { + // TL country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Turkmenistan" -> { + // TM country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Tunisia" -> { + // TN country code + min_zoom = 7.7f; + max_zoom = 11.0f; + } + case "Tonga" -> { + // TO country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + // Turkey + case "Turkiye" -> { + // TR country code + min_zoom = 7.0f; + max_zoom = 11.0f; + } + case "Trinidad and Tobago" -> { + // TT country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Tuvalu" -> { + // TV country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Taiwan" -> { + // TW country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "Tanzania" -> { + // TZ country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Ukraine" -> { + // UA country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + case "Uganda" -> { + // UG country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "U.S. Minor Outlying Islands" -> { + // UM country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "United States of America" -> { + // US country code + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Uruguay" -> { + // UY country code + min_zoom = 8.0f; + max_zoom = 11.0f; + } + case "Uzbekistan" -> { + // UZ country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Vatican" -> { + // VA country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "Saint Vincent and Grenadines" -> { + // VC country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Venezuela" -> { + // VE country code + min_zoom = 7.1f; + max_zoom = 11.3f; + } + case "British Virgin Islands" -> { + // VG country code + min_zoom = 18.0f; + max_zoom = 18.0f; + } + case "U.S. Virgin Islands" -> { + // VI country code + min_zoom = 11.0f; + max_zoom = 11.0f; + } + case "Vietnam" -> { + // VN country code + min_zoom = 8.3f; + max_zoom = 11.0f; + } + case "Vanuatu" -> { + // VU country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Wallis and Futuna Islands" -> { + // WF country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Samoa" -> { + // WS country code + min_zoom = 9.0f; + max_zoom = 11.0f; + } + case "Kosovo" -> { + // XK country code + min_zoom = 10.0f; + max_zoom = 11.0f; + } + case "Yemen" -> { + // YE country code + min_zoom = 8.7f; + max_zoom = 11.0f; + } + case "South Africa" -> { + // ZA country code + min_zoom = 4.6f; + max_zoom = 10.1f; + } + case "Zambia" -> { + // ZM country code + min_zoom = 6.6f; + max_zoom = 11.0f; + } + case "Zimbabwe" -> { + // ZW country code + min_zoom = 6.7f; + max_zoom = 11.0f; + } + } + } + } catch (Exception e) { + } + + float[] zoomArray = new float[2]; + zoomArray[0] = min_zoom - 1; + zoomArray[1] = max_zoom - 1; + + return zoomArray; + } +} diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/FeatureId.java b/tiles/src/main/java/com/protomaps/basemap/feature/FeatureId.java index e5f65808..6996d9df 100644 --- a/tiles/src/main/java/com/protomaps/basemap/feature/FeatureId.java +++ b/tiles/src/main/java/com/protomaps/basemap/feature/FeatureId.java @@ -5,8 +5,8 @@ import com.onthegomap.planetiler.reader.osm.OsmSourceFeature; public class FeatureId { - public static long create(SourceFeature feature) { - if (feature instanceof OsmSourceFeature osmFeature) { + public static long create(SourceFeature sf) { + if (sf instanceof OsmSourceFeature osmFeature) { long elemType; var element = osmFeature.originalElement(); if (element instanceof OsmElement.Relation) { @@ -18,6 +18,6 @@ public static long create(SourceFeature feature) { } return (elemType << 44) | element.id(); } - return feature.id(); + return sf.id(); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/feature/RegionNameZooms.java b/tiles/src/main/java/com/protomaps/basemap/feature/RegionNameZooms.java new file mode 100644 index 00000000..dedce01d --- /dev/null +++ b/tiles/src/main/java/com/protomaps/basemap/feature/RegionNameZooms.java @@ -0,0 +1,316 @@ +package com.protomaps.basemap.feature; + +import com.onthegomap.planetiler.reader.SourceFeature; + +public class RegionNameZooms { + + public static float[] getMinMaxZooms(SourceFeature sf) { + float min_zoom = 8.0f; // default for all regions + float max_zoom = 11.0f; // default for all regions + + try { + var name = sf.getString("name:en") == null ? sf.getString("name") : sf.getString("name:en"); + + if (name != null) { + switch (name) { + // United States + case "Alabama" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Alaska" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Arizona" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Arkansas" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "California" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Colorado" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Connecticut" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Delaware" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Florida" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Georgia" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Hawaii" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Idaho" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Illinois" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Indiana" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Iowa" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Kansas" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Kentucky" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Louisiana" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Maine" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Maryland" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Massachusetts" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Michigan" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Minnesota" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Mississippi" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Missouri" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Montana" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Nebraska" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Nevada" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "New Hampshire" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "New Jersey" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "New Mexico" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "New York" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "North Carolina" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "North Dakota" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Ohio" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Oklahoma" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Oregon" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Pennsylvania" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Rhode Island" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "South Carolina" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "South Dakota" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Tennessee" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Texas" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Utah" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Vermont" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Virginia" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Washington" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "West Virginia" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Wisconsin" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Wyoming" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + + // Canada + case "Alberta" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "British Columbia" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Manitoba" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "New Brunswick" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Newfoundland and Labrador" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Northwest Territories" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Nova Scotia" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Nunavut" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Ontario" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Prince Edward Island" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Quebec" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Saskatchewan" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + case "Yukon" -> { + min_zoom = 3.5f; + max_zoom = 7.5f; + } + + // Australia + case "New South Wales" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Queensland" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "South Australia" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Tasmania" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Victoria" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Western Australia" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Australian Capital Territory" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + case "Northern Territory" -> { + min_zoom = 4.6f; + max_zoom = 8.1f; + } + } + } + } catch (Exception e) { + } + + float[] zoomArray = new float[2]; + zoomArray[0] = min_zoom - 1; + zoomArray[1] = max_zoom - 1; + + return zoomArray; + } +} diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Boundaries.java b/tiles/src/main/java/com/protomaps/basemap/layers/Boundaries.java index e52d3678..2ac442db 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Boundaries.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Boundaries.java @@ -21,9 +21,153 @@ public String name() { return "boundaries"; } + public void processNe(SourceFeature sf, FeatureCollector features) { + var sourceLayer = sf.getSourceLayer(); + var kind = ""; + var kind_detail = ""; + var admin_level = 2; + var disputed = false; + var theme_min_zoom = 0; + var theme_max_zoom = 0; + + if (sourceLayer.equals("ne_50m_admin_0_boundary_lines_land") || + sourceLayer.equals("ne_50m_admin_0_boundary_lines_disputed_areas") || + sourceLayer.equals("ne_50m_admin_0_boundary_lines_maritime_indicator_chn") || + sourceLayer.equals("ne_50m_admin_1_states_provinces_lines")) { + theme_min_zoom = 1; + theme_max_zoom = 3; + kind = "tz_boundary"; + } else if (sourceLayer.equals("ne_10m_admin_0_boundary_lines_land") || + sourceLayer.equals("ne_10m_admin_0_boundary_lines_map_units") || + sourceLayer.equals("ne_10m_admin_0_boundary_lines_disputed_areas") || + sourceLayer.equals("ne_10m_admin_0_boundary_lines_maritime_indicator_chn") || + sourceLayer.equals("ne_10m_admin_1_states_provinces_lines")) { + theme_min_zoom = 4; + theme_max_zoom = 5; + kind = "tz_boundary"; + } + + // TODO (nvkelso 2023-03-26) + // Compiler is fussy about booleans and strings, beware + if (kind != "") { + switch (sf.getString("featurecla")) { + case "Disputed (please verify)" -> { + kind = "country"; + kind_detail = "disputed"; + disputed = true; + } + case "Indefinite (please verify)" -> { + kind = "country"; + kind_detail = "indefinite"; + disputed = true; + } + case "Indeterminant frontier" -> { + kind = "country"; + kind_detail = "indeterminant"; + disputed = true; + } + case "International boundary (verify)" -> kind = "country"; + case "Lease limit" -> { + kind = "lease_limit"; + admin_level = 3; + } + case "Line of control (please verify)" -> { + kind = "country"; + kind_detail = "line_of_control"; + disputed = true; + } + case "Overlay limit" -> { + kind = "overlay_limit"; + admin_level = 3; + } + case "Unrecognized" -> kind = "unrecognized_country"; + case "Map unit boundary" -> { + kind = "map_unit"; + admin_level = 3; + } + case "Breakaway" -> { + kind = "unrecognized_country"; + kind_detail = "disputed_breakaway"; + admin_level = 3; + } + case "Claim boundary" -> { + kind = "unrecognized_country"; + kind_detail = "disputed_claim"; + admin_level = 3; + } + case "Elusive frontier" -> { + kind = "unrecognized_country"; + kind_detail = "disputed_elusive"; + admin_level = 3; + } + case "Reference line" -> { + kind = "unrecognized_country"; + kind_detail = "disputed_reference_line"; + admin_level = 3; + } + case "Admin-1 region boundary" -> { + kind = "macroregion"; + admin_level = 3; + } + case "Admin-1 boundary" -> { + kind = "region"; + admin_level = 4; + } + case "Admin-1 statistical boundary" -> { + kind = "region"; + admin_level = 4; + } + case "Admin-1 statistical meta bounds" -> { + kind = "region"; + admin_level = 4; + } + case "1st Order Admin Lines" -> { + kind = "region"; + admin_level = 4; + } + case "Unrecognized Admin-1 region boundary" -> { + kind = "unrecognized_macroregion"; + admin_level = 4; + } + case "Unrecognized Admin-1 boundary" -> { + kind = "unrecognized_region"; + admin_level = 4; + } + case "Unrecognized Admin-1 statistical boundary" -> { + kind = "unrecognized_region"; + admin_level = 4; + } + case "Unrecognized Admin-1 statistical meta bounds" -> { + kind = "unrecognized_region"; + admin_level = 4; + } + default -> kind = ""; + } + } + + if (sf.canBeLine() && sf.hasTag("min_zoom") && (kind.equals("") == false && kind.equals("tz_boundary") == false)) { + features.line(this.name()) + // Don't label lines to reduce file size (and they aren't shown in styles anyhow) + //.setAttr("name", sf.getString("name")) + .setAttr("pmap:min_zoom", sf.getLong("min_zoom")) + .setAttr("pmap:min_admin_level", admin_level) + .setZoomRange( + sf.getString("min_zoom") == null ? theme_min_zoom : (int) Double.parseDouble(sf.getString("min_zoom")), + theme_max_zoom) + .setAttr("pmap:ne_id", sf.getString("ne_id")) + .setAttr("pmap:brk_a3", sf.getString("brk_a3")) + .setAttr("pmap:kind", kind) + .setAttr("pmap:kind_detail", kind_detail) + .setAttr("disputed", disputed) + .setBufferPixels(8); + } + } + @Override public void processFeature(SourceFeature sf, FeatureCollector features) { - if (sf.canBeLine()) { + if (sf.canBeLine() && sf.hasTag("admin_level")) { + // Beware coastlines and coastal waters (eg with admin borders in large estuaries) + // like mouth of Columbia River between Oregon and Washington in USA if (sf.hasTag("natural", "coastline") || sf.hasTag("maritime", "yes")) { return; } @@ -31,19 +175,59 @@ public void processFeature(SourceFeature sf, FeatureCollector features) { if (recs.size() > 0) { OptionalInt minAdminLevel = recs.stream().mapToInt(r -> r.relation().adminLevel).min(); OptionalInt disputed = recs.stream().mapToInt(r -> r.relation().disputed).max(); - var line = - features.line(this.name()).setId(FeatureId.create(sf)).setMinPixelSize(0).setAttr("pmap:min_admin_level", - minAdminLevel.getAsInt()); - if (minAdminLevel.getAsInt() <= 2) { - line.setMinZoom(0); - } else if (minAdminLevel.getAsInt() <= 4) { - line.setMinZoom(3); - } else { - line.setMinZoom(10); - } - - if (disputed.getAsInt() == 1) { - line.setAttr("disputed", 1); + + var kind = ""; + var kind_detail = ""; + + var min_zoom = 0; + var theme_min_zoom = 6; + + // Core Tilezen schema properties + switch (minAdminLevel.getAsInt()) { + case 2 -> { + kind = "country"; + kind_detail = "2"; + // While country boundary lines should show up very early + min_zoom = 0; + // Natural Earth is used for low zooms (for compilation and tile size reasons) + theme_min_zoom = 6; + } + case 4 -> { + kind = "region"; + kind_detail = "4"; + // While region boundary lines should show up early-zooms + min_zoom = 6; + // Natural Earth is used for low zooms (for compilation and tile size reasons) + theme_min_zoom = 6; + } + case 6 -> { + kind = "county"; + kind_detail = "6"; + min_zoom = 8; + theme_min_zoom = 8; + } + case 8 -> { + kind = "locality"; + kind_detail = "8"; + min_zoom = 10; + theme_min_zoom = 10; + } + } + + if (kind != "" && kind_detail != "") { + var line = features.line(this.name()) + .setId(FeatureId.create(sf)) + .setMinPixelSize(0) + .setAttr("pmap:min_admin_level", minAdminLevel.getAsInt()) + .setAttr("pmap:kind", kind) + .setAttr("pmap:kind_detail", kind_detail) + .setAttr("pmap:min_zoom", min_zoom) + .setMinZoom(theme_min_zoom); + + // Core Tilezen schema properties + if (disputed.getAsInt() == 1) { + line.setAttr("disputed", 1); + } } } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Buildings.java b/tiles/src/main/java/com/protomaps/basemap/layers/Buildings.java index bae7f64d..d2483a15 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Buildings.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Buildings.java @@ -9,7 +9,6 @@ import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; import com.protomaps.basemap.feature.FeatureId; -import com.protomaps.basemap.names.OsmNames; import com.protomaps.basemap.postprocess.Area; import java.util.List; @@ -20,10 +19,31 @@ public String name() { return "buildings"; } + static int quantize_val(double val, int step) { + // special case: if val is very small, we don't want it rounding to zero, so + // round the smallest values up to the first step. + if (val < step) { + return (int) step; + } + + return (int) Math.round(val / step) * step; + } + @Override public void processFeature(SourceFeature sf, FeatureCollector features) { - if (sf.canBePolygon() && (sf.hasTag("building") || sf.hasTag("building:part"))) { + if (sf.canBePolygon() && ((sf.hasTag("building") && !sf.hasTag("building", "no")) || + (sf.hasTag("building:part") && !sf.hasTag("building:part", "no")))) { Double height = parseDoubleOrNull(sf.getString("height")); + Double min_height = parseDoubleOrNull(sf.getString("min_height")); + Integer min_zoom = 11; + String kind = "building"; + + // Limit building:part features to later zooms + // TODO: (nvkelso 20230621) this should be based on area and volume, too + if (sf.hasTag("building:part")) { + kind = "building_part"; + min_zoom = 14; + } if (height == null) { Double levels = parseDoubleOrNull(sf.getString("building:levels")); @@ -34,12 +54,23 @@ public void processFeature(SourceFeature sf, FeatureCollector features) { var feature = features.polygon(this.name()) .setId(FeatureId.create(sf)) - .setAttrWithMinzoom("building:part", sf.getString("building:part"), 13) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // Core OSM tags for different kinds of places .setAttrWithMinzoom("layer", sf.getString("layer"), 13) - .setAttrWithMinzoom("height", height, 13) - .setZoomRange(10, 15); + // NOTE: Height is quantized by zoom in a post-process step + .setAttr("height", height) + .setZoomRange(min_zoom, 15); - OsmNames.setOsmNames(feature, sf, 13); + if (kind == "building_part") { + // We don't need to set WithMinzoom because that's implicate with the ZoomRange + feature.setAttr("pmap:kind_detail", sf.getString("building:part")); + feature.setAttr("min_height", sf.getString("min_height")); + } + + // Names should mostly just be for POIs + // Sometimes building name and address are useful items, but only at zoom 17+ + //OsmNames.setOsmNames(feature, sf, 13); } } @@ -50,8 +81,64 @@ public List postProcess(int zoom, List i } items = Area.filterArea(items, 0); - if (zoom >= 14) + // DEBUG + //items = Area.addAreaTag(items); + + if (zoom >= 15) return items; + + // quantize height by zoom when less than max_zoom 15 to facilitate better feature merging + for (var item : items) { + try { + if (item.attrs().containsKey("height")) { + var height = (double) item.attrs().get("height"); + + // Protected against NULL values + if (height > 0) { + // at zoom <= 12 round height to nearest 20 meters + if (zoom <= 12) { + height = quantize_val(height, 20); + } else + // at zoom 13 round height to nearest 10 meters + if (zoom == 13) { + height = quantize_val(height, 10); + } else + // at zoom 14 round height to nearest 5 meters + if (zoom == 14) { + height = quantize_val(height, 5); + } + + item.attrs().put("height", height); + } + } + } catch (Exception e) { + } + + try { + if (item.attrs().containsKey("min_height")) { + var min_height = (double) item.attrs().get("min_height"); + + // Protected against NULL values + if (min_height > 0) { + if (zoom <= 12) { + min_height = quantize_val(min_height, 20); + } else + // at zoom 13 round height to nearest 10 meters + if (zoom == 13) { + min_height = quantize_val(min_height, 10); + } else + // at zoom 14 round height to nearest 5 meters + if (zoom == 14) { + min_height = quantize_val(min_height, 5); + } + + item.attrs().put("min_height", min_height); + } + } + } catch (Exception e) { + } + } + return FeatureMerge.mergeNearbyPolygons(items, 3.125, 3.125, 0.5, 0.5); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Earth.java b/tiles/src/main/java/com/protomaps/basemap/layers/Earth.java index 017e665b..ad1418c4 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Earth.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Earth.java @@ -17,17 +17,18 @@ public String name() { public void processOsm(SourceFeature sf, FeatureCollector features) { features.polygon(this.name()) + .setAttr("pmap:kind", "earth") .setZoomRange(6, 15).setBufferPixels(8); } public void processNe(SourceFeature sf, FeatureCollector features) { var sourceLayer = sf.getSourceLayer(); if (sourceLayer.equals("ne_110m_land")) { - features.polygon(this.name()).setZoomRange(0, 1); + features.polygon(this.name()).setZoomRange(0, 1).setBufferPixels(8).setAttr("pmap:kind", "earth"); } else if (sourceLayer.equals("ne_50m_land")) { - features.polygon(this.name()).setZoomRange(2, 4); + features.polygon(this.name()).setZoomRange(2, 4).setBufferPixels(8).setAttr("pmap:kind", "earth"); } else if (sourceLayer.equals("ne_10m_land")) { - features.polygon(this.name()).setZoomRange(5, 5); + features.polygon(this.name()).setZoomRange(5, 5).setBufferPixels(8).setAttr("pmap:kind", "earth"); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Landuse.java b/tiles/src/main/java/com/protomaps/basemap/layers/Landuse.java index b4e8869a..1bfbb2bd 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Landuse.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Landuse.java @@ -6,7 +6,6 @@ import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; import com.protomaps.basemap.feature.FeatureId; -import com.protomaps.basemap.names.OsmNames; import com.protomaps.basemap.postprocess.Area; import java.util.List; @@ -18,62 +17,161 @@ public static void processFeature(SourceFeature sf, FeatureCollector features, S if (sf.canBePolygon() && (sf.hasTag("aeroway", "aerodrome", "runway") || sf.hasTag("area:aeroway", "taxiway", "runway") || sf.hasTag("amenity", "hospital", "school", "kindergarten", "university", "college") || + sf.hasTag("boundary", "national_park", "protected_area") || sf.hasTag("landuse", "recreation_ground", "industrial", "brownfield", "railway", "cemetery", "commercial", - "grass", "orchard", "farmland", "farmyard", "residential") || - sf.hasTag("leisure", "park", "garden", "golf_course", "dog_park", "playground", "pitch") || + "grass", "orchard", "farmland", "farmyard", "residential", "military") || + sf.hasTag("leisure", "park", "garden", "golf_course", "dog_park", "playground", "pitch", "nature_reserve") || sf.hasTag("man_made", "pier") || + sf.hasTag("natural", "beach") || + // TODO: (nvkelso 20230622) This use of the place tag here is dubious, though paired with "residential" sf.hasTag("place", "neighbourhood") || sf.hasTag("railway", "platform") || + sf.hasTag("tourism", "zoo") || (sf.hasTag("area", "yes") && (sf.hasTag("highway", "pedestrian", "footway") || sf.hasTag("man_made", "bridge"))))) { + String kind = "other"; + if (sf.hasTag("aeroway", "aerodrome")) { + kind = sf.getString("aeroway"); + } else if (sf.hasTag("amenity", "university", "college", "hospital", "library", "post_office", "school", + "townhall")) { + kind = sf.getString("amenity"); + } else if (sf.hasTag("amenity", "cafe")) { + kind = sf.getString("amenity"); + } else if (sf.hasTag("highway")) { + kind = "pedestrian"; + } else if (sf.hasTag("landuse", "cemetery")) { + kind = sf.getString("landuse"); + } else if (sf.hasTag("landuse", "orchard", "farmland", "farmyard")) { + kind = "farmland"; + } else if (sf.hasTag("landuse", "residential")) { + kind = "residential"; + } else if (sf.hasTag("landuse", "industrial", "brownfield")) { + kind = "industrial"; + } else if (sf.hasTag("landuse", "military")) { + kind = "military"; + if (sf.hasTag("military", "naval_base", "airfield")) { + kind = sf.getString("military"); + } + } else if (sf.hasTag("leisure", "golf_course", "marina", "park", "stadium")) { + kind = sf.getString("leisure"); + } else if (sf.hasTag("man_made", "bridge")) { + kind = "pedestrian"; + } else if (sf.hasTag("man_made", "pier")) { + kind = "pier"; + } else if (sf.hasTag("shop", "grocery", "supermarket")) { + kind = sf.getString("shop"); + } else if (sf.hasTag("tourism", "attraction", "camp_site", "hotel")) { + kind = sf.getString("tourism"); + } else { + // Avoid problem of too many "other" kinds + // All these will default to min_zoom of 15 + // If a more specific min_zoom is needed (or sanitize kind values) + // then add new logic in section above + if (sf.hasTag("amenity")) { + kind = sf.getString("amenity"); + } else if (sf.hasTag("craft")) { + kind = sf.getString("craft"); + } else if (sf.hasTag("aeroway")) { + kind = sf.getString("aeroway"); + } else if (sf.hasTag("historic")) { + kind = sf.getString("historic"); + } else if (sf.hasTag("landuse")) { + kind = sf.getString("landuse"); + } else if (sf.hasTag("leisure")) { + kind = sf.getString("leisure"); + } else if (sf.hasTag("man_made")) { + kind = sf.getString("man_made"); + } else if (sf.hasTag("natural")) { + kind = sf.getString("natural"); + } else if (sf.hasTag("railway")) { + kind = sf.getString("railway"); + } else if (sf.hasTag("shop")) { + kind = sf.getString("shop"); + } else if (sf.hasTag("tourism")) { + kind = sf.getString("tourism"); + // Boundary is most generic, so place last else we loose out + // on nature_reserve detail versus all the protected_area + } else if (sf.hasTag("boundary")) { + kind = sf.getString("boundary"); + } + } + + // National forests + if (sf.hasTag("boundary", "national_park") && + sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service")) { + kind = "forest"; + } else if (sf.hasTag("boundary", "national_park") && + sf.hasTag("protect_class", "6") && + sf.hasTag("protection_title", "National Forest")) { + kind = "forest"; + } else if (sf.hasTag("landuse", "forest") && + sf.hasTag("protect_class", "6")) { + kind = "forest"; + } else if (sf.hasTag("landuse", "forest") && + sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service")) { + kind = "forest"; + } else if (sf.hasTag("landuse", "forest")) { + kind = "forest"; + } else if (sf.hasTag("boundary", "protected_area") && + sf.hasTag("protect_class", "6") && + sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service")) { + kind = "forest"; + } + + // National parks + if (sf.hasTag("boundary", "national_park")) { + if (!(sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service") || + sf.hasTag("protection_title", "Conservation Area", "Conservation Park", "Environmental use", "Forest Reserve", + "National Forest", "National Wildlife Refuge", "Nature Refuge", "Nature Reserve", "Protected Site", + "Provincial Park", "Public Access Land", "Regional Reserve", "Resources Reserve", "State Forest", + "State Game Land", "State Park", "Watershed Recreation Unit", "Wild Forest", "Wilderness Area", + "Wilderness Study Area", "Wildlife Management", "Wildlife Management Area", "Wildlife Sanctuary")) && + (sf.hasTag("protect_class", "2", "3") || + sf.hasTag("operator", "United States National Park Service", "National Park Service", + "US National Park Service", "U.S. National Park Service", "US National Park service") || + sf.hasTag("operator:en", "Parks Canada") || + sf.hasTag("designation", "national_park") || + sf.hasTag("protection_title", "National Park"))) { + kind = "national_park"; + } else { + kind = "park"; + } + } + var poly = features.polygon(layerName) .setId(FeatureId.create(sf)) - .setAttr("landuse", sf.getString("landuse")) - .setAttr("leisure", sf.getString("leisure")) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // Core OSM tags for different kinds of places + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail .setAttr("aeroway", sf.getString("aeroway")) - .setAttr("area:aeroway", sf.getString("area:aeroway")) .setAttr("amenity", sf.getString("amenity")) + .setAttr("area:aeroway", sf.getString("area:aeroway")) + .setAttr("boundary", sf.getString("boundary")) .setAttr("highway", sf.getString("highway")) + .setAttr("landuse", sf.getString("landuse")) + .setAttr("leisure", sf.getString("leisure")) .setAttr("man_made", sf.getString("man_made")) + .setAttr("natural", sf.getString("natural")) .setAttr("place", sf.getString("place")) .setAttr("railway", sf.getString("railway")) .setAttr("sport", sf.getString("sport")) - .setZoomRange(5, 15) - .setMinPixelSize(3.0); + // NOTE: (nvkelso 20230622) Consider zoom 5 instead... + // But to match Protomaps v2 we do earlier + .setZoomRange(2, 15) + .setMinPixelSize(2.0); - OsmNames.setOsmNames(poly, sf, 0); - - if (ghostFeatures) { - poly.setAttr("isGhostFeature", true); - } - - poly.setAttr("pmap:area", ""); - - String kind = "other"; - if (sf.hasTag("leisure")) { - kind = "park"; - } else if (sf.hasTag("amenity")) { - if (sf.hasTag("amenity", "hospital")) { - kind = "hospital"; - } else { - kind = "school"; - } - } else if (sf.hasTag("landuse")) { - if (sf.hasTag("landuse", "orchard", "farmland", "farmyard")) { - kind = "farmland"; - } else if (sf.hasTag("landuse", "industrial", "brownfield")) { - kind = "industrial"; - } else if (sf.hasTag("landuse", "cemetery")) { - kind = "cemetery"; - } - } else if (sf.hasTag("highway")) { - kind = "pedestrian"; - } else if (sf.hasTag("man_made", "bridge")) { - kind = "pedestrian"; - } else if (sf.hasTag("aeroway", "aerodrome")) { - kind = "aerodrome"; - } - poly.setAttr("pmap:kind", kind); + // NOTE: (nvkelso 20230622) landuse labels for polygons are found in the pois layer + //OsmNames.setOsmNames(poly, sf, 0); } } @@ -89,7 +187,7 @@ public void processFeature(SourceFeature sf, FeatureCollector features) { @Override public List postProcess(int zoom, List items) throws GeometryException { - items = Area.addAreaTag(items); + //items = Area.addAreaTag(items); if (zoom == 15) return items; int minArea = 400 / (4096 * 4096) * (256 * 256); @@ -98,6 +196,8 @@ public List postProcess(int zoom, List i else if (zoom <= 5) minArea = 800 / (4096 * 4096) * (256 * 256); items = Area.filterArea(items, minArea); + + //return FeatureMerge.mergeNearbyPolygons(items, 3.125, 3.125, 0.5, 0.5); return items; } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Natural.java b/tiles/src/main/java/com/protomaps/basemap/layers/Natural.java index 5a4d64fb..c96fa8cc 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Natural.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Natural.java @@ -1,12 +1,11 @@ package com.protomaps.basemap.layers; import com.onthegomap.planetiler.FeatureCollector; +import com.onthegomap.planetiler.FeatureMerge; import com.onthegomap.planetiler.ForwardingProfile; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; -import com.protomaps.basemap.feature.FeatureId; -import com.protomaps.basemap.names.OsmNames; import com.protomaps.basemap.postprocess.Area; import java.util.List; @@ -19,33 +18,48 @@ public String name() { @Override public void processFeature(SourceFeature sf, FeatureCollector features) { - if (sf.canBePolygon() && (sf.hasTag("natural", "wood", "glacier", "scrub", "sand", "wetland", "bare_rock") || - sf.hasTag("landuse", "forest", "meadow") || sf.hasTag("leisure", "nature_reserve") || - sf.hasTag("boundary", "national_park", "protected_area"))) { + if (sf.canBePolygon() && + (sf.hasTag("natural", "wood", "glacier", "grass", "scrub", "sand", "wetland", "bare_rock") || + sf.hasTag("landuse", "forest", "meadow", "grass"))) { + String kind = "other"; + if (sf.hasTag("natural")) { + kind = sf.getString("natural"); + } else if (sf.hasTag("landuse")) { + kind = sf.getString("landuse"); + } + var feat = features.polygon(this.name()) - .setId(FeatureId.create(sf)) + //.setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // Core OSM tags for different kinds of places + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail .setAttr("natural", sf.getString("natural")) - .setAttr("boundary", sf.getString("boundary")) .setAttr("landuse", sf.getString("landuse")) - .setAttr("leisure", sf.getString("leisure")) - .setZoomRange(5, 15) - .setMinPixelSize(3.0); + // NOTE: (nvkelso 20230622) Consider zoom 5 instead... + // But to match Protomaps v2 we do earlier + .setZoomRange(2, 15) + .setMinPixelSize(2.0); - OsmNames.setOsmNames(feat, sf, 0); + // NOTE: (nvkelso 20230622) landuse labels for polygons are found in the pois layer + //OsmNames.setOsmNames(feat, sf, 0); } } @Override public List postProcess(int zoom, List items) throws GeometryException { - items = Area.addAreaTag(items); + //items = Area.addAreaTag(items); if (zoom == 15) return items; + int minArea = 400 / (4096 * 4096) * (256 * 256); if (zoom == 6) minArea = 600 / (4096 * 4096) * (256 * 256); else if (zoom <= 5) minArea = 800 / (4096 * 4096) * (256 * 256); items = Area.filterArea(items, minArea); - return items; + + return FeatureMerge.mergeNearbyPolygons(items, 3.125, 3.125, 0.5, 0.5); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalLine.java b/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalLine.java index 81557200..ecde2e31 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalLine.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalLine.java @@ -19,29 +19,48 @@ public String name() { public void processFeature(SourceFeature sf, FeatureCollector features) { if (sf.canBeLine() && (sf.hasTag("waterway") || sf.hasTag("natural", "strait", "cliff")) && (!sf.hasTag("waterway", "riverbank", "reservoir"))) { + int min_zoom = 12; + String kind = "other"; + if (sf.hasTag("waterway")) { + kind = sf.getString("waterway"); + if (sf.hasTag("waterway", "river")) { + min_zoom = 9; + } + } else if (sf.hasTag("natural")) { + kind = sf.getString("natural"); + } + var feat = features.line(this.name()) .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // Core OSM tags for different kinds of places + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail .setAttr("waterway", sf.getString("waterway")) .setAttr("natural", sf.getString("natural")) - // Add less common attributes only at higher zooms - .setAttrWithMinzoom("bridge", sf.getString("bridge"), 12) - .setAttrWithMinzoom("tunnel", sf.getString("tunnel"), 12) + // Add less common core Tilezen attributes only at higher zooms (will continue to v4) + //.setAttrWithMinzoom("bridge", sf.getString("bridge"), 12) + //.setAttrWithMinzoom("tunnel", sf.getString("tunnel"), 12) .setAttrWithMinzoom("layer", sf.getString("layer"), 12) - .setZoomRange(12, 15); + .setZoomRange(min_zoom, 15); + // Add less common core Tilezen attributes only at higher zooms (will continue to v4) if (sf.hasTag("intermittent", "yes")) { - feat.setAttr("intermittent", 1); + feat.setAttr("intermittent", true); } - String kind = "other"; - if (sf.hasTag("waterway")) { - kind = "waterway"; - } else if (sf.hasTag("natural")) { - kind = "natural"; + // Set "brunnel" (bridge / tunnel) property where "level" = 1 is a bridge, 0 is ground level, and -1 is a tunnel + // Because of MapLibre performance and draw order limitations, generally the boolean is sufficent + // See also: "layer" for more complicated ±6 layering for more sophisticated graphics libraries + if (sf.hasTag("bridge") && !sf.hasTag("bridge", "no")) { + feat.setAttrWithMinzoom("pmap:level", 1, 12); + } else if (sf.hasTag("tunnel") && !sf.hasTag("tunnel", "no")) { + feat.setAttrWithMinzoom("pmap:level", -1, 12); + } else { + feat.setAttrWithMinzoom("pmap:level", 0, 12); } - feat.setAttr("pmap:kind", kind); - OsmNames.setOsmNames(feat, sf, 0); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalPoint.java b/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalPoint.java index 6eef1b7f..10ed096c 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalPoint.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/PhysicalPoint.java @@ -3,8 +3,11 @@ import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.ForwardingProfile; import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; import com.protomaps.basemap.feature.FeatureId; +import com.protomaps.basemap.names.NeNames; import com.protomaps.basemap.names.OsmNames; import java.util.List; @@ -15,22 +18,72 @@ public String name() { return "physical_point"; } + private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = + Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); + + public void processNe(SourceFeature sf, FeatureCollector features) { + var sourceLayer = sf.getSourceLayer(); + var kind = ""; + var alkaline = 0; + var reservoir = 0; + var theme_min_zoom = 0; + var theme_max_zoom = 0; + + if (sourceLayer.equals("ne_10m_lakes")) { + theme_min_zoom = 5; + theme_max_zoom = 5; + + switch (sf.getString("featurecla")) { + case "Alkaline Lake" -> { + kind = "lake"; + alkaline = 1; + } + case "Lake" -> kind = "lake"; + case "Reservoir" -> { + kind = "lake"; + reservoir = 1; + } + case "Playa" -> kind = "playa"; + } + + if (kind != "" && sf.hasTag("min_label") && sf.hasTag("name") && sf.getTag("name") != null) { + var water_label_position = features.pointOnSurface(this.name()) + .setAttr("pmap:kind", kind) + .setAttr("pmap:min_zoom", sf.getLong("min_label") + 1) + .setZoomRange(sf.getString("min_label") == null ? theme_min_zoom : + (int) Double.parseDouble(sf.getString("min_label")) + 1, theme_max_zoom) + .setBufferPixels(128); + + NeNames.setNeNames(water_label_position, sf, 0); + } + } + } + @Override public void processFeature(SourceFeature sf, FeatureCollector features) { if (sf.isPoint() && (sf.hasTag("place", "sea", "ocean") || sf.hasTag("natural", "peak"))) { // TODO: rank based on ele + String kind = ""; + int minzoom = 12; - if (sf.hasTag("natural", "peak")) { - minzoom = 13; + if (sf.hasTag("place", "ocean")) { + kind = "ocean"; + minzoom = 0; } if (sf.hasTag("place", "sea")) { + kind = "sea"; minzoom = 3; } + if (sf.hasTag("natural", "peak")) { + kind = "peak"; + minzoom = 13; + } var feat = features.point(this.name()) .setId(FeatureId.create(sf)) + .setAttr("pmap:kind", kind) .setAttr("place", sf.getString("place")) .setAttr("natural", sf.getString("natural")) .setAttr("ele", sf.getString("ele")) @@ -38,10 +91,133 @@ public void processFeature(SourceFeature sf, FeatureCollector features) { OsmNames.setOsmNames(feat, sf, 0); } + + if (sf.hasTag("name") && sf.getTag("name") != null && + sf.canBePolygon() && + (sf.hasTag("water") || + sf.hasTag("waterway") || + // bay, straight, fjord are included here only (not in water layer) because + // OSM treats them as "overlay" label features over the normal water polys + sf.hasTag("natural", "water", "bay", "strait", "fjord") || + sf.hasTag("landuse", "reservoir") || + sf.hasTag("leisure", "swimming_pool"))) { + String kind = "other"; + var kind_detail = ""; + var name_min_zoom = 15; + var reservoir = false; + var alkaline = false; + Double way_area = 0.0; + + try { + way_area = sf.area() / WORLD_AREA_FOR_70K_SQUARE_METERS; + } catch (GeometryException e) { + System.out.println(e); + } + + // coallese values across tags to single kind value + if (sf.hasTag("natural", "water", "bay", "strait", "fjord")) { + kind = sf.getString("natural"); + if (sf.hasTag("water", "basin", "canal", "ditch", "drain", "lake", "river", "stream")) { + kind_detail = sf.getString("water"); + + // This is a bug in Tilezen v1.9 that should be fixed in 2.0 + // But isn't present in Protomaps v2 so let's fix it preemtively + if (kind_detail == "lake") { + kind = "lake"; + } + + if (sf.hasTag("water", "lagoon", "oxbow", "pond", "reservoir", "wastewater")) { + kind_detail = "lake"; + } + if (sf.hasTag("water", "reservoir")) { + reservoir = true; + } + if (sf.hasTag("water", "lagoon", "salt", "salt_pool")) { + alkaline = true; + } + } + } else if (sf.hasTag("waterway", "riverbank", "dock", "canal", "river", "stream", "ditch", "drain")) { + kind = sf.getString("waterway"); + } else if (sf.hasTag("landuse", "basin", "reservoir")) { + kind = sf.getString("landuse"); + } else if (sf.hasTag("leisure", "swimming_pool")) { + kind = "swimming_pool"; + } else if (sf.hasTag("amenity", "swimming_pool")) { + kind = "swimming_pool"; + } + + // We don't want to show too many water labels at early zooms else it crowds the map + // TODO: (nvkelso 20230621) These numbers are super wonky, they should instead be sq meters in web mercator prj + // Zoom 5 and earlier from Natural Earth instead (see above) + if (way_area > 25000) { //500000000 + name_min_zoom = 6; + } else if (way_area > 8000) { //500000000 + name_min_zoom = 7; + } else if (way_area > 3000) { //200000000 + name_min_zoom = 8; + } else if (way_area > 500) { //40000000 + name_min_zoom = 9; + } else if (way_area > 200) { //8000000 + name_min_zoom = 10; + } else if (way_area > 30) { //1000000 + name_min_zoom = 11; + } else if (way_area > 25) { //500000 + name_min_zoom = 12; + } else if (way_area > 0.5) { //50000 + name_min_zoom = 13; + } else if (way_area > 0.05) { //10000 + name_min_zoom = 14; + } + + var water_label_position = features.pointOnSurface(this.name()) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + .setAttr("pmap:kind_detail", kind_detail) + // While other layers don't need min_zoom, physical point labels do for more + // predictable client-side label collisions + // 512 px zooms versus 256 px logical zooms + .setAttr("pmap:min_zoom", name_min_zoom + 1) + // DEBUG + //.setAttr("pmap:area", way_area) + // + // Core OSM tags for different kinds of places + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail + .setAttr("natural", sf.getString("natural")) + .setAttr("landuse", sf.getString("landuse")) + .setAttr("leisure", sf.getString("leisure")) + .setAttr("water", sf.getString("water")) + .setAttr("waterway", sf.getString("waterway")) + // Add less common core Tilezen attributes only at higher zooms (will continue to v4) + .setAttrWithMinzoom("bridge", sf.getString("bridge"), 12) + .setAttrWithMinzoom("tunnel", sf.getString("tunnel"), 12) + .setAttrWithMinzoom("layer", sf.getString("layer"), 12) + .setZoomRange(name_min_zoom, 15) + .setBufferPixels(128); + + // Add less common core Tilezen attributes only at higher zooms (will continue to v4) + if (kind_detail != "") { + water_label_position.setAttr("pmap:kind_detail", kind_detail); + } + if (sf.hasTag("water", "reservoir") || reservoir) { + water_label_position.setAttr("reservoir", true); + } + if (sf.hasTag("water", "lagoon", "salt", "salt_pool") || alkaline) { + water_label_position.setAttr("alkaline", true); + } + if (sf.hasTag("intermittent", "yes")) { + water_label_position.setAttr("intermittent", true); + } + + OsmNames.setOsmNames(water_label_position, sf, 0); + } } @Override public List postProcess(int zoom, List items) { + // DEBUG + //items = Area.addAreaTag(items); + return items; } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Places.java b/tiles/src/main/java/com/protomaps/basemap/layers/Places.java index 25288660..e5478617 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Places.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Places.java @@ -6,11 +6,15 @@ import com.onthegomap.planetiler.ForwardingProfile; import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.util.ZoomFunction; +import com.protomaps.basemap.feature.CountryNameZooms; import com.protomaps.basemap.feature.FeatureId; +import com.protomaps.basemap.feature.RegionNameZooms; import com.protomaps.basemap.names.NeNames; import com.protomaps.basemap.names.OsmNames; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class Places implements ForwardingProfile.FeatureProcessor, ForwardingProfile.FeaturePostProcessor { @@ -19,6 +23,24 @@ public String name() { return "places"; } + private final AtomicInteger placeNumber = new AtomicInteger(0); + + // Evaluates place layer sort ordering of inputs into an integer for the sort-key field. + /* + static int getSortKey(float min_zoom, int population_rank, long population, String name) { + return SortKey + // ORDER BY "min_zoom" ASC NULLS LAST, + .orderByDouble(min_zoom == null ? 15.0 : min_zoom, 0.0, 15.0, 1) + // population_rank DESC NULLS LAST, + .thenByInt(population_rank == null ? 15 : population_rank, 0, 15) + // population DESC NULLS LAST, + .thenByLog(population, 1, 1 << (SORT_KEY_BITS - 13) - 1) + // length(name) ASC + .thenByInt(name == null ? 0 : name.length(), 0, 31) + .get(); + } + */ + public void processNe(SourceFeature sf, FeatureCollector features) { var sourceLayer = sf.getSourceLayer(); var kind = ""; @@ -28,7 +50,7 @@ public void processNe(SourceFeature sf, FeatureCollector features) { var theme_max_zoom = 0; if (sourceLayer.equals("ne_10m_populated_places")) { theme_min_zoom = 1; - theme_max_zoom = 8; + theme_max_zoom = 6; } // Test for props because of Natural Earth funk @@ -37,14 +59,14 @@ public void processNe(SourceFeature sf, FeatureCollector features) { case "Admin-0 capital": case "Admin-0 capital alt": case "Admin-0 region capital": - kind = "city"; + kind = "locality"; break; case "Admin-1 capital": case "Admin-1 region capital": - kind = "city"; + kind = "locality"; break; case "Populated place": - kind = "city"; + kind = "locality"; break; case "Historic place": kind = "locality"; @@ -57,6 +79,8 @@ public void processNe(SourceFeature sf, FeatureCollector features) { } } + var min_zoom = sf.getString("min_zoom") == null ? 10 : (int) Double.parseDouble(sf.getString("min_zoom")); + int population_rank = sf.getString("rank_max") == null ? 0 : (int) Double.parseDouble(sf.getString("rank_max")); if (kind != "") { var feat = features.point(this.name()) .setAttr("name", sf.getString("name")) @@ -66,10 +90,13 @@ public void processNe(SourceFeature sf, FeatureCollector features) { theme_max_zoom) .setAttr("pmap:kind", kind) .setAttr("pmap:kind_detail", kind_detail) - .setAttr("population", sf.getString("pop_max")) - .setAttr("population_rank", sf.getString("rank_max")) + .setAttr("population", parseIntOrNull(sf.getString("pop_max"))) + .setAttr("pmap:population_rank", population_rank) .setAttr("wikidata_id", sf.getString("wikidata")) - .setBufferPixels(128); + .setBufferPixels(128) + // we set the sort keys so the label grid can be sorted predictably (bonus: tile features also sorted) + .setSortKey(min_zoom) + .setPointLabelGridPixelSize(7, 16); NeNames.setNeNames(feat, sf, 0); } @@ -78,59 +105,67 @@ public void processNe(SourceFeature sf, FeatureCollector features) { @Override public void processFeature(SourceFeature sf, FeatureCollector features) { if (sf.isPoint() && - (sf.hasTag("place", "suburb", "town", "village", "neighbourhood", "city", "country", "state", "province"))) { - Integer population = - sf.getString("population") == null ? 0 : parseIntOrNull(sf.getString("population")); - var feat = features.point(this.name()) - .setId(FeatureId.create(sf)) - .setAttr("place", sf.getString("place")) - .setAttr("country_code_iso3166_1_alpha_2", sf.getString("country_code_iso3166_1_alpha_2")) - .setAttr("capital", sf.getString("capital")); - - OsmNames.setOsmNames(feat, sf, 0); - - if (sf.hasTag("place", "country")) { - feat.setAttr("pmap:kind", "country") - .setZoomRange(0, 9); - } else if (sf.hasTag("place", "state", "province")) { - feat.setAttr("pmap:kind", "state") - .setZoomRange(4, 11); - } else if (sf.hasTag("place", "city")) { - feat.setAttr("pmap:kind", "city") - .setZoomRange(8, 15); - if (population.equals(0)) { - population = 10000; - } - } else if (sf.hasTag("place", "town")) { - feat.setAttr("pmap:kind", "neighbourhood") - .setZoomRange(8, 15); - if (population.equals(0)) { - population = 5000; - } - } else if (sf.hasTag("place", "village")) { - feat.setAttr("pmap:kind", "neighbourhood") - .setZoomRange(10, 15); - if (population.equals(0)) { - population = 2000; - } - } else if (sf.hasTag("place", "suburb")) { - feat.setAttr("pmap:kind", "neighbourhood") - .setZoomRange(8, 15); - } else { - feat.setAttr("pmap:kind", "neighbourhood") - .setZoomRange(12, 15); - } + (sf.hasTag("place", "suburb", "town", "village", "neighbourhood", "quarter", "city", "country", "state", + "province"))) { + String kind = "other"; + int min_zoom = 12; + int max_zoom = 15; + long population = sf.getString("population") == null ? 0 : parseIntOrNull(sf.getString("population")); + int population_rank = 0; + String place = sf.getString("place"); - if (population != null) { - feat.setAttr("population", population); - feat.setSortKey((int) Math.log(population)); - // TODO: use label grid - } else { - feat.setSortKey(0); + switch (place) { + case "country": + kind = "country"; + min_zoom = (int) CountryNameZooms.getMinMaxZooms(sf)[0]; + max_zoom = (int) CountryNameZooms.getMinMaxZooms(sf)[1]; + break; + case "state": + case "province": + kind = "region"; + min_zoom = (int) RegionNameZooms.getMinMaxZooms(sf)[0]; + max_zoom = (int) RegionNameZooms.getMinMaxZooms(sf)[1]; + break; + case "city": + case "town": + kind = "locality"; + // TODO: these should be from data join to Natural Earth, and if fail data join then default to 8 + min_zoom = 7; + max_zoom = 15; + if (population == 0) { + if (place.equals("town")) { + population = 10000; + } else { + population = 5000; + } + } + break; + case "village": + kind = "locality"; + // TODO: these should be from data join to Natural Earth, and if fail data join then default to 8 + min_zoom = 10; + max_zoom = 15; + if (population == 0) { + population = 2000; + } + break; + case "suburb": + kind = "neighbourhood"; + min_zoom = 11; + max_zoom = 15; + break; + case "quarter": + kind = "macrohood"; + min_zoom = 10; + max_zoom = 15; + break; + case "neighbourhood": + kind = "neighbourhood"; + min_zoom = 12; + max_zoom = 15; + break; } - int population_rank = 0; - int[] pop_breaks = { 1000000000, 100000000, @@ -158,7 +193,38 @@ public void processFeature(SourceFeature sf, FeatureCollector features) { } } - feat.setAttr("population_rank", population_rank); + var feat = features.point(this.name()) + .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + .setAttr("pmap:kind_detail", place) + .setAttr("pmap:min_zoom", min_zoom + 1) + // Core OSM tags for different kinds of places + .setAttr("capital", sf.getString("capital")) + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should be a kind, or included in kind_detail + .setAttr("place", sf.getString("place")) + .setAttr("country_code_iso3166_1_alpha_2", sf.getString("country_code_iso3166_1_alpha_2")) + .setZoomRange(min_zoom, max_zoom); + + if (population > 0) { + feat.setAttr("population", population) + .setAttr("pmap:population_rank", population_rank); + + feat.setSortKey(min_zoom * 1000 + 400 - population_rank * 200 + placeNumber.incrementAndGet()); + //feat.setSortKey(getSortKey("pmap:min_zoom", "pmap:population_rank", "population", "name")); + } else { + feat.setSortKey(min_zoom * 1000); + } + + OsmNames.setOsmNames(feat, sf, 0); + + // we set the sort keys so the label grid can be sorted predictably (bonus: tile features also sorted) + feat.setPointLabelGridSizeAndLimit(12, 5, 2); + + // and also whenever you set a label grid size limit, make sure you increase the buffer size so no + // label grid squares will be the consistent between adjacent tiles + feat.setBufferPixelOverrides(ZoomFunction.maxZoom(12, 32)); } } @@ -170,7 +236,7 @@ public List postProcess(int zoom, List i List noncities = new ArrayList<>(); for (VectorTile.Feature item : items) { - if (item.attrs().get("pmap:kind").equals("city")) { + if (item.attrs().get("pmap:kind").equals("locality")) { cities.add(item); } else { noncities.add(item); @@ -182,6 +248,8 @@ public List postProcess(int zoom, List i List top64 = cities.subList(startIndex, endIndex); + // This should always be less than 64 now that a label grid is being used + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling for (int i = 0; i < top64.size(); i++) { if (top64.size() - i < 16) { top64.get(i).attrs().put("pmap:rank", 1); diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java index 245751e8..3f440623 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Pois.java @@ -1,12 +1,18 @@ package com.protomaps.basemap.layers; +import static com.onthegomap.planetiler.util.Parse.parseDoubleOrNull; + import com.onthegomap.planetiler.FeatureCollector; import com.onthegomap.planetiler.ForwardingProfile; import com.onthegomap.planetiler.VectorTile; +import com.onthegomap.planetiler.geo.GeoUtils; +import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; +import com.onthegomap.planetiler.util.ZoomFunction; import com.protomaps.basemap.feature.FeatureId; import com.protomaps.basemap.names.OsmNames; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; public class Pois implements ForwardingProfile.FeatureProcessor, ForwardingProfile.FeaturePostProcessor { @@ -15,73 +21,493 @@ public String name() { return "pois"; } + private static final double WORLD_AREA_FOR_70K_SQUARE_METERS = + Math.pow(GeoUtils.metersToPixelAtEquator(0, Math.sqrt(70_000)) / 256d, 2); + private static final double LOG2 = Math.log(2); + + /* + * Assign every toilet a monotonically increasing ID so that we can limit output at low zoom levels to only the + * highest ID toilet nodes. Be sure to use thread-safe data structures any time a profile holds state since multiple + * threads invoke processFeature concurrently. + */ + private final AtomicInteger poiNumber = new AtomicInteger(0); + @Override public void processFeature(SourceFeature sf, FeatureCollector features) { - if (!sf.canBeLine() && (sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("amenity") || - sf.hasTag("shop") || - sf.hasTag("tourism") || + if ((sf.isPoint() || sf.canBePolygon()) && (sf.hasTag("aeroway", "aerodrome") || + sf.hasTag("amenity") || + sf.hasTag("attraction") || + sf.hasTag("boundary", "national_park", "protected_area") || + sf.hasTag("craft") || + sf.hasTag("historic") || + sf.hasTag("landuse", "cemetery", "recreation_ground", "winter_sports", "quarry", "park", "forest", "military") || sf.hasTag("leisure") || - sf.hasTag("aeroway", "aerodrome") || - sf.hasTag("railway", "station"))) { + sf.hasTag("natural", "beach") || + sf.hasTag("railway", "station") || + sf.hasTag("shop") || + sf.hasTag("tourism") && + (!sf.hasTag("historic", "district")))) { String kind = "other"; + String kind_detail = ""; + Integer min_zoom = 15; + if (sf.hasTag("aeroway", "aerodrome")) { kind = sf.getString("aeroway"); - } else if (sf.hasTag("amenity", "cafe", "college", "hospital", "library", "post_office", "school", "townhall")) { + min_zoom = 13; + + // Emphasize large international airports earlier + if (kind == "aerodrome" && sf.hasTag("iata")) { + min_zoom -= 2; + } + + if (sf.hasTag("aerodrome")) { + kind_detail = sf.getString("aerodrome"); + } + } else if (sf.hasTag("amenity", "university", "college")) { + kind = sf.getString("amenity"); + // One would think University should be earlier, but there are lots of dinky node only places + // So if the university has a large area, it'll naturally improve it's zoom in the next section... + min_zoom = 14; + } else if (sf.hasTag("amenity", "hospital")) { + kind = sf.getString("amenity"); + min_zoom = 12; + } else if (sf.hasTag("amenity", "library", "post_office", "townhall")) { kind = sf.getString("amenity"); + min_zoom = 13; + } else if (sf.hasTag("amenity", "school")) { + kind = sf.getString("amenity"); + min_zoom = 15; + } else if (sf.hasTag("amenity", "cafe")) { + kind = sf.getString("amenity"); + min_zoom = 15; } else if (sf.hasTag("landuse", "cemetery")) { kind = sf.getString("landuse"); + min_zoom = 14; + } else if (sf.hasTag("landuse", "military")) { + kind = "military"; + if (sf.hasTag("military", "naval_base", "airfield")) { + kind = sf.getString("military"); + } } else if (sf.hasTag("leisure", "golf_course", "marina", "park", "stadium")) { kind = sf.getString("leisure"); - } else if (sf.hasTag("leisure")) { - // This is dubious but existing behavior - kind = "park"; + min_zoom = 13; } else if (sf.hasTag("shop", "grocery", "supermarket")) { kind = sf.getString("shop"); + min_zoom = 14; } else if (sf.hasTag("tourism", "attraction", "camp_site", "hotel")) { kind = sf.getString("tourism"); + min_zoom = 15; + } else { + // Avoid problem of too many "other" kinds + // All these will default to min_zoom of 15 + // If a more specific min_zoom is needed (or sanitize kind values) + // then add new logic in section above + if (sf.hasTag("amenity")) { + kind = sf.getString("amenity"); + } else if (sf.hasTag("attraction")) { + kind = sf.getString("attraction"); + } else if (sf.hasTag("craft")) { + kind = sf.getString("craft"); + } else if (sf.hasTag("aeroway")) { + kind = sf.getString("aeroway"); + } else if (sf.hasTag("landuse")) { + kind = sf.getString("landuse"); + } else if (sf.hasTag("leisure")) { + kind = sf.getString("leisure"); + } else if (sf.hasTag("natural")) { + kind = sf.getString("natural"); + } else if (sf.hasTag("railway")) { + kind = sf.getString("railway"); + } else if (sf.hasTag("shop")) { + kind = sf.getString("shop"); + } else if (sf.hasTag("tourism")) { + kind = sf.getString("tourism"); + // Boundary is most generic, so place last else we loose out + // on nature_reserve detail versus all the protected_area + } else if (sf.hasTag("historic") && !sf.hasTag("historic", "yes")) { + kind = sf.getString("historic"); + } else if (sf.hasTag("boundary")) { + kind = sf.getString("boundary"); + } + } + + // National forests + if (sf.hasTag("boundary", "national_park") && + sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service")) { + kind = "forest"; + } else if (sf.hasTag("boundary", "national_park") && + sf.hasTag("protect_class", "6") && + sf.hasTag("protection_title", "National Forest")) { + kind = "forest"; + } else if (sf.hasTag("landuse", "forest") && + sf.hasTag("protect_class", "6")) { + kind = "forest"; + } else if (sf.hasTag("landuse", "forest") && + sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service")) { + kind = "forest"; + } else if (sf.hasTag("landuse", "forest")) { + kind = "forest"; + } else if (sf.hasTag("boundary", "protected_area") && + sf.hasTag("protect_class", "6") && + sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service")) { + kind = "forest"; + } + + // National parks + if (sf.hasTag("boundary", "national_park")) { + if (!(sf.hasTag("operator", "United States Forest Service", "US Forest Service", "U.S. Forest Service", + "USDA Forest Service", "United States Department of Agriculture", "US National Forest Service", + "United State Forest Service", "U.S. National Forest Service") || + sf.hasTag("protection_title", "Conservation Area", "Conservation Park", "Environmental use", "Forest Reserve", + "National Forest", "National Wildlife Refuge", "Nature Refuge", "Nature Reserve", "Protected Site", + "Provincial Park", "Public Access Land", "Regional Reserve", "Resources Reserve", "State Forest", + "State Game Land", "State Park", "Watershed Recreation Unit", "Wild Forest", "Wilderness Area", + "Wilderness Study Area", "Wildlife Management", "Wildlife Management Area", "Wildlife Sanctuary")) && + (sf.hasTag("protect_class", "2", "3") || + sf.hasTag("operator", "United States National Park Service", "National Park Service", + "US National Park Service", "U.S. National Park Service", "US National Park service") || + sf.hasTag("operator:en", "Parks Canada") || + sf.hasTag("designation", "national_park") || + sf.hasTag("protection_title", "National Park"))) { + kind = "national_park"; + min_zoom = 11; + } else { + kind = "park"; + } + } + + if (sf.hasTag("cuisine")) { + kind_detail = sf.getString("cuisine"); + } else if (sf.hasTag("religion")) { + kind_detail = sf.getString("religion"); + } else if (sf.hasTag("sport")) { + kind_detail = sf.getString("sport"); } // try first for polygon -> point representations - if (sf.canBePolygon()) { + if (sf.canBePolygon() && sf.hasTag("name") && sf.getString("name") != null) { + Double way_area = 0.0; + try { + way_area = sf.worldGeometry().getEnvelopeInternal().getArea() / WORLD_AREA_FOR_70K_SQUARE_METERS; + } catch (GeometryException e) { + System.out.println(e); + } + + Double height = 0.0; + try { + height = sf.getString("height") == null ? 0.0 : parseDoubleOrNull(sf.getString("height")); + } catch (Exception e) { + System.out.println("Problem getting height"); + } + + // Area zoom grading overrides the kind zoom grading in the section above. + // Roughly shared with the water label area zoom grading in physical points layer + // + // Allowlist of kind values eligible for early zoom point labels + if (kind.equals("national_park")) { + if (way_area > 300000) { // 500000000 sq meters (web mercator proj) + min_zoom = 5; + } else if (way_area > 25000) { // 500000000 sq meters (web mercator proj) + min_zoom = 6; + } else if (way_area > 10000) { // 500000000 + min_zoom = 7; + } else if (way_area > 2000) { // 200000000 + min_zoom = 8; + } else if (way_area > 250) { // 40000000 + min_zoom = 9; + } else if (way_area > 100) { // 8000000 + min_zoom = 10; + } else if (way_area > 20) { // 500000 + min_zoom = 11; + } else if (way_area > 1) { // 50000 + min_zoom = 12; + } else if (way_area > 0.2) { // 10000 + min_zoom = 13; + } + } else if (kind.equals("aerodrome") || + kind.equals("golf_course") || + kind.equals("military") || + kind.equals("naval_base") || + kind.equals("stadium") || + kind.equals("zoo")) { + //if (way_area > 300000) { // 500000000 sq meters (web mercator proj) + // min_zoom = 5; + //} else if (way_area > 25000) { // 500000000 sq meters (web mercator proj) + // min_zoom = 6; + //} else + if (way_area > 20000) { // 500000000 + min_zoom = 7; + } else if (way_area > 5000) { // 200000000 + min_zoom = 8; + } else if (way_area > 250) { // 40000000 + min_zoom = 9; + } else if (way_area > 100) { // 8000000 + min_zoom = 10; + } else if (way_area > 20) { // 500000 + min_zoom = 11; + } else if (way_area > 1) { // 50000 + min_zoom = 12; + } else if (way_area > 0.2) { // 10000 + min_zoom = 13; + } + } else if (kind.equals("college") || + kind.equals("university")) { + if (way_area > 20000) { + min_zoom = 7; + } else if (way_area > 5000) { + min_zoom = 8; + } else if (way_area > 250) { + min_zoom = 9; + } else if (way_area > 150) { + min_zoom = 10; + } else if (way_area > 100) { + min_zoom = 11; + } else if (way_area > 50) { + min_zoom = 12; + } else if (way_area > 20) { + min_zoom = 13; + } else if (way_area > 5) { + min_zoom = 14; + } else { + min_zoom = 15; + } + } else if (kind.equals("forest") || + kind.equals("park") || + kind.equals("protected_area") || + kind.equals("nature_reserve")) { + if (way_area > 10000) { + min_zoom = 7; + } else if (way_area > 4000) { + min_zoom = 8; + } else if (way_area > 1000) { + min_zoom = 9; + } else if (way_area > 250) { + min_zoom = 10; + } else if (way_area > 20) { + min_zoom = 11; + } else if (way_area > 5) { + min_zoom = 12; + } else if (way_area > 0.5) { + min_zoom = 13; + } else if (way_area > 0.1) { + min_zoom = 14; + } else if (way_area > 0.01) { + min_zoom = 15; + } else if (way_area > 0.001) { + min_zoom = 16; + } else { + min_zoom = 17; + } + } else if (kind.equals("cemetery") || + kind.equals("school")) { + if (way_area > 5) { + min_zoom = 12; + } else if (way_area > 1) { + min_zoom = 13; + } else if (way_area > 0.1) { + min_zoom = 14; + } else if (way_area > 0.01) { + min_zoom = 15; + } else { + min_zoom = 16; + } + // Typically for "building" derived label placements for shops and other businesses + } else { + if (way_area > 10) { + min_zoom = 11; + } else if (way_area > 2) { + min_zoom = 12; + } else if (way_area > 0.5) { + min_zoom = 13; + } else if (way_area > 0.01) { + min_zoom = 14; + } + + // Small but tall features should show up early as they have regional prominance. + // Height measured in meters + if (min_zoom >= 13 && height > 0.0) { + if (height >= 100) { + min_zoom = 11; + } else if (height >= 20) { + min_zoom = 12; + } else if (height >= 10) { + min_zoom = 13; + } + + // Clamp certain kind values so medium tall buildings don't crowd downtown areas + // NOTE: (nvkelso 20230623) Apply label grid to early zooms of POIs layer + // NOTE: (nvkelso 20230624) Turn this into an allowlist instead of a blocklist + if (kind.equals("hotel") || kind.equals("hostel") || kind.equals("parking") || kind.equals("bank") || + kind.equals("place_of_worship") || kind.equals("jewelry") || kind.equals("yes") || + kind.equals("restaurant") || kind.equals("coworking_space") || kind.equals("clothes") || + kind.equals("art") || kind.equals("school")) { + if (min_zoom == 12) { + min_zoom = 13; + } + } + + // Discount tall university buildings, require a related university landuse AOI + if (kind.equals("university")) { + min_zoom = 13; + } + } + } + var poly_label_position = features.pointOnSurface(this.name()) // all POIs should receive their IDs at all zooms // (there is no merging of POIs like with lines and polygons in other layers) .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions + // 512 px zooms versus 256 px logical zooms + .setAttr("pmap:min_zoom", (int) (min_zoom + 1)) + // + // DEBUG + //.setAttr("pmap:area_debug", way_area) + // + // Core OSM tags for different kinds of places + // Special airport only tag (to indicate if it's an airport with regular commercial flights) + .setAttr("iata", sf.getString("iata")) + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should be a kind, or included in kind_detail .setAttr("amenity", sf.getString("amenity")) - .setAttr("shop", sf.getString("shop")) + .setAttr("attraction", sf.getString("attraction")) + .setAttr("craft", sf.getString("craft")) + .setAttr("historic", sf.getString("historic")) + .setAttr("landuse", sf.getString("landuse")) + .setAttr("leisure", sf.getString("leisure")) + .setAttr("natural", sf.getString("natural")) .setAttr("railway", sf.getString("railway")) + .setAttr("shop", sf.getString("shop")) + .setAttr("tourism", sf.getString("tourism")) + // Extra OSM tags for certain kinds of places + // These are duplicate of what's in the kind_detail tag + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should be a kind, or included in kind_detail .setAttr("cuisine", sf.getString("cuisine")) .setAttr("religion", sf.getString("religion")) - .setAttr("tourism", sf.getString("tourism")) - .setAttr("iata", sf.getString("iata")) - .setZoomRange(13, 15) + .setAttr("sport", sf.getString("sport")) + .setZoomRange(min_zoom, 15) .setBufferPixels(128); + // Core Tilezen schema properties + if (kind_detail != "") { + poly_label_position.setAttr("pmap:kind_detail", kind_detail); + } + OsmNames.setOsmNames(poly_label_position, sf, 0); - poly_label_position.setAttr("pmap:kind", kind); + // NOTE: (nvkelso 20230627) This could also include other params like the name + poly_label_position.setSortKey(min_zoom * 1000 + poiNumber.incrementAndGet()); + + // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown + // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 + poly_label_position.setPointLabelGridSizeAndLimit(14, 12, 1); + + // and also whenever you set a label grid size limit, make sure you increase the buffer size so no + // label grid squares will be the consistent between adjacent tiles + poly_label_position.setBufferPixelOverrides(ZoomFunction.maxZoom(14, 32)); + } else if (sf.isPoint()) { var point_feature = features.point(this.name()) + // all POIs should receive their IDs at all zooms + // (there is no merging of POIs like with lines and polygons in other layers) .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // While other layers don't need min_zoom, POIs do for more predictable client-side label collisions + // 512 px zooms versus 256 px logical zooms + .setAttr("pmap:min_zoom", (int) (min_zoom + 1)) + // Core OSM tags for different kinds of places + // Special airport only tag (to indicate if it's an airport with regular commercial flights) + .setAttr("iata", sf.getString("iata")) + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail .setAttr("amenity", sf.getString("amenity")) - .setAttr("shop", sf.getString("shop")) + .setAttr("attraction", sf.getString("attraction")) + .setAttr("craft", sf.getString("craft")) + .setAttr("historic", sf.getString("historic")) + .setAttr("landuse", sf.getString("landuse")) + .setAttr("leisure", sf.getString("leisure")) + .setAttr("natural", sf.getString("natural")) .setAttr("railway", sf.getString("railway")) + .setAttr("shop", sf.getString("shop")) + .setAttr("tourism", sf.getString("tourism")) + // Extra OSM tags for certain kinds of places + // These are duplicate of what's in the kind_detail tag + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail .setAttr("cuisine", sf.getString("cuisine")) .setAttr("religion", sf.getString("religion")) - .setAttr("tourism", sf.getString("tourism")) - .setAttr("iata", sf.getString("iata")) - .setZoomRange(13, 15) + .setAttr("sport", sf.getString("sport")) + .setZoomRange(min_zoom, 15) .setBufferPixels(128); + // Core Tilezen schema properties + if (kind_detail != "") { + point_feature.setAttr("pmap:kind_detail", kind_detail); + } + OsmNames.setOsmNames(point_feature, sf, 0); - point_feature.setAttr("pmap:kind", kind); + // Some features should only be visible at very late zooms when they don't have a name + if (sf.hasTag("name") == false && (sf.hasTag("amenity", "atm", "bbq", "bench", "bicycle_parking", + "bicycle_rental", "bicycle_repair_station", "boat_storage", "bureau_de_change", "car_rental", "car_sharing", + "car_wash", "charging_station", "customs", "drinking_water", "fuel", "harbourmaster", "hunting_stand", + "karaoke_box", "life_ring", "money_transfer", "motorcycle_parking", "parking", "picnic_table", "post_box", + "ranger_station", "recycling", "sanitary_dump_station", "shelter", "shower", "taxi", "telephone", "toilets", + "waste_basket", "waste_disposal", "water_point", "watering_place", "bicycle_rental", "motorcycle_parking", + "charging_station") || + sf.hasTag("historic", "landmark", "wayside_cross") || + sf.hasTag("leisure", "dog_park", "firepit", "fishing", "pitch", "playground", "slipway", "swimming_area") || + sf.hasTag("tourism", "alpine_hut", "information", "picnic_site", "viewpoint", "wilderness_hut"))) { + point_feature.setAttr("pmap:min_zoom", 17); + } + + if (sf.hasTag("amenity", "clinic", "dentist", "doctors", "social_facility", "baby_hatch", "childcare", + "car_sharing", "bureau_de_change", "emergency_phone", "karaoke", "karaoke_box", "money_transfer", "car_wash", + "hunting_stand", "studio", "boat_storage", "gambling", "adult_gaming_centre", "sanitary_dump_station", + "attraction", "animal", "water_slide", "roller_coaster", "summer_toboggan", "carousel", "amusement_ride", + "maze") || + sf.hasTag("historic", "memorial", "district") || + sf.hasTag("leisure", "pitch", "playground", "slipway") || + sf.hasTag("shop", "scuba_diving", "atv", "motorcycle", "snowmobile", "art", "bakery", "beauty", "bookmaker", + "books", "butcher", "car", "car_parts", "car_repair", "clothes", "computer", "convenience", "fashion", + "florist", "garden_centre", "gift", "golf", "greengrocer", "grocery", "hairdresser", "hifi", "jewelry", + "lottery", "mobile_phone", "newsagent", "optician", "perfumery", "ship_chandler", "stationery", "tobacco", + "travel_agency") || + sf.hasTag("tourism", "artwork", "hanami", "trail_riding_station", "bed_and_breakfast", "chalet", + "guest_house", "hostel")) { + point_feature.setAttr("pmap:min_zoom", 17); + } + + // NOTE: (nvkelso 20230627) This could also include other params like the name + point_feature.setSortKey(min_zoom * 1000 + poiNumber.incrementAndGet()); + + // Even with the categorical zoom bucketing above, we end up with too dense a point feature spread in downtown + // areas, so cull the labels which wouldn't label at earlier zooms than the max_zoom of 15 + point_feature.setPointLabelGridSizeAndLimit(14, 12, 1); + + // and also whenever you set a label grid size limit, make sure you increase the buffer size so no + // label grid squares will be the consistent between adjacent tiles + point_feature.setBufferPixelOverrides(ZoomFunction.maxZoom(14, 32)); } } } @Override - public List postProcess(int zoom, List items) { + public List postProcess(int zoom, List items) throws GeometryException { + + // TODO: (nvkelso 20230623) Consider adding a "pmap:rank" here for POIs, like for Places + + //items = Area.addAreaTag(items); return items; } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java b/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java index ec29205a..881c6f88 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Roads.java @@ -20,13 +20,20 @@ public String name() { } @Override - public void processFeature(SourceFeature sourceFeature, FeatureCollector features) { - if (sourceFeature.canBeLine() && sourceFeature.hasTag("highway") && - !(sourceFeature.hasTag("highway", "proposed", "abandoned", "razed", "demolished", "removed", "construction"))) { - - String highway = sourceFeature.getString("highway"); - String shield_text = sourceFeature.getString("ref"); - String network_val = sourceFeature.getString("network"); + public void processFeature(SourceFeature sf, FeatureCollector features) { + if (sf.canBeLine() && sf.hasTag("highway") && + !(sf.hasTag("highway", "proposed", "abandoned", "razed", "demolished", "removed", "construction"))) { + String kind = "other"; + String kind_detail = ""; + int min_zoom = 15; + int max_zoom = 15; + int min_zoom_shield_text = 10; + int min_zoom_names = 14; + + String highway = sf.getString("highway"); + String service = ""; + String shield_text = sf.getString("ref"); + String network_val = sf.getString("network"); shield_text = (shield_text == null ? null : shield_text.split(";")[0]); if (shield_text != null) { if (shield_text.contains("US ")) { @@ -36,118 +43,155 @@ public void processFeature(SourceFeature sourceFeature, FeatureCollector feature shield_text = shield_text.replaceAll("I ", ""); network_val = "US:I"; } else { + // This should be replaced by walking the way's relations (which reliably set network) network_val = "other"; } } shield_text = (shield_text == null ? null : shield_text.replaceAll("\\s", "")); Integer shield_text_length = (shield_text == null ? null : shield_text.length()); - var feat = features.line("roads") - .setId(FeatureId.create(sourceFeature)) - .setMinPixelSize(0) - .setPixelTolerance(0) - .setAttr("highway", highway) - .setAttrWithMinzoom("bridge", sourceFeature.getString("bridge"), 12) - .setAttrWithMinzoom("tunnel", sourceFeature.getString("tunnel"), 12) - .setAttrWithMinzoom("layer", sourceFeature.getString("layer"), 12) - .setAttrWithMinzoom("oneway", sourceFeature.getString("oneway"), 14); - if (highway.equals("motorway") || highway.equals("motorway_link")) { - feat.setAttr("pmap:kind", "highway").setZoomRange(6, 15); + // TODO: (nvkelso 20230622) Use Natural Earth for low zoom roads at zoom 5 and earlier + // as normally OSM roads would start at 6, but we start at 3 to match Protomaps v2 + kind = "highway"; + min_zoom = 3; if (highway.equals("motorway")) { - feat.setAttrWithMinzoom("ref", shield_text, 7) - .setAttrWithMinzoom("ref_length", shield_text_length, 7) - .setAttrWithMinzoom("network", network_val, 7); + min_zoom_shield_text = 7; } else { - feat.setAttrWithMinzoom("ref", shield_text, 12) - .setAttrWithMinzoom("ref_length", shield_text_length, 12) - .setAttrWithMinzoom("network", network_val, 12); + min_zoom_shield_text = 12; } - OsmNames.setOsmNames(feat, sourceFeature, 11); + min_zoom_names = 11; } else if (highway.equals("trunk") || highway.equals("trunk_link") || highway.equals("primary") || highway.equals("primary_link")) { - feat.setAttr("pmap:kind", "major_road").setZoomRange(7, 15); + kind = "major_road"; + min_zoom = 7; if (highway.equals("trunk")) { - feat.setAttrWithMinzoom("ref", shield_text, 8) - .setAttrWithMinzoom("ref_length", shield_text_length, 8) - .setAttrWithMinzoom("network", network_val, 8); + // Just trunk earlier zoom, otherwise road network looks choppy just with motorways then + min_zoom = 6; + min_zoom_shield_text = 8; } else if (highway.equals("primary")) { - feat.setAttrWithMinzoom("ref", shield_text, 10) - .setAttrWithMinzoom("ref_length", shield_text_length, 10) - .setAttrWithMinzoom("network", network_val, 10); + min_zoom_shield_text = 10; } else if (highway.equals("trunk_link")) { - feat.setAttrWithMinzoom("ref", shield_text, 12) - .setAttrWithMinzoom("ref_length", shield_text_length, 12) - .setAttrWithMinzoom("network", network_val, 12); + min_zoom_shield_text = 12; } else { - feat.setAttrWithMinzoom("ref", shield_text, 13) - .setAttrWithMinzoom("ref_length", shield_text_length, 13) - .setAttrWithMinzoom("network", network_val, 13); + min_zoom_shield_text = 13; } - OsmNames.setOsmNames(feat, sourceFeature, 12); + min_zoom_names = 12; } else if (highway.equals("secondary") || highway.equals("secondary_link") || highway.equals("tertiary") || highway.equals("tertiary_link")) { - feat.setAttr("pmap:kind", "medium_road").setZoomRange(9, 15); + kind = "medium_road"; + min_zoom = 9; if (highway.equals("secondary")) { - feat.setAttrWithMinzoom("ref", shield_text, 11) - .setAttrWithMinzoom("ref_length", shield_text_length, 11) - .setAttrWithMinzoom("network", network_val, 11); + min_zoom_shield_text = 11; + min_zoom_names = 12; } else if (highway.equals("tertiary")) { - feat.setAttrWithMinzoom("ref", shield_text, 12) - .setAttrWithMinzoom("ref_length", shield_text_length, 12) - .setAttrWithMinzoom("network", network_val, 12); + min_zoom_shield_text = 12; + min_zoom_names = 13; } else { - feat.setAttrWithMinzoom("ref", shield_text, 13) - .setAttrWithMinzoom("ref_length", shield_text_length, 13) - .setAttrWithMinzoom("network", network_val, 13); + min_zoom_shield_text = 13; + min_zoom_names = 14; } - OsmNames.setOsmNames(feat, sourceFeature, 13); } else if (highway.equals("residential") || highway.equals("service") || highway.equals("unclassified") || highway.equals("road") || highway.equals("raceway")) { - feat.setAttr("pmap:kind", "minor_road").setZoomRange(12, 15) - .setAttrWithMinzoom("ref", shield_text, 12) - .setAttrWithMinzoom("ref_length", shield_text_length, 12) - .setAttrWithMinzoom("network", network_val, 12); - OsmNames.setOsmNames(feat, sourceFeature, 14); - } else if (sourceFeature.hasTag("highway", "pedestrian", "track", "path", "cycleway", "bridleway", "footway", + kind = "minor_road"; + min_zoom = 12; + min_zoom_shield_text = 12; + min_zoom_names = 14; + + if (highway.equals("service")) { + kind_detail = "service"; + min_zoom = 13; + + // push down "alley", "driveway", "parking_aisle", "drive-through" & etc + if (sf.hasTag("service")) { + min_zoom = 14; + service = sf.getString("service"); + } + } + } else if (sf.hasTag("highway", "pedestrian", "track", "path", "cycleway", "bridleway", "footway", "steps", "corridor")) { - feat.setAttr("pmap:kind_detail", highway).setZoomRange(12, 15) - .setAttrWithMinzoom("ref", shield_text, 12) - .setAttrWithMinzoom("ref_length", shield_text_length, 12) - .setAttrWithMinzoom("network", network_val, 12) - .setAttr("pmap:kind", "path").setZoomRange(12, 15); - OsmNames.setOsmNames(feat, sourceFeature, 14); + kind = "path"; + kind_detail = highway; + min_zoom = 12; + min_zoom_shield_text = 12; + min_zoom_names = 14; + + if (sf.hasTag("highway", "path", "cycleway", "bridleway", "footway", "steps")) { + min_zoom = 13; + } + if (sf.hasTag("footway", "sidewalk", "crossing")) { + min_zoom = 14; + kind_detail = sf.getString("footway"); + } + if (sf.hasTag("highway", "corridor")) { + min_zoom = 14; + } + } else { + kind = "other"; + kind_detail = sf.getString("service"); + min_zoom = 14; + min_zoom_shield_text = 14; + min_zoom_names = 14; + } + + var feat = features.line("roads") + .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // To power better client label collisions + .setAttr("pmap:min_zoom", min_zoom + 1) + .setAttrWithMinzoom("ref", shield_text, min_zoom_shield_text) + .setAttrWithMinzoom("shield_text_length", shield_text_length, min_zoom_shield_text) + .setAttrWithMinzoom("network", network_val, min_zoom_shield_text) + // Core OSM tags for different kinds of places + .setAttrWithMinzoom("layer", sf.getString("layer"), 12) + .setAttrWithMinzoom("oneway", sf.getString("oneway"), 14) + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail + .setAttr("highway", highway) + .setMinPixelSize(0) + .setPixelTolerance(0) + .setZoomRange(min_zoom, max_zoom); + + // Core Tilezen schema properties + if (kind_detail != "") { + feat.setAttr("pmap:kind_detail", kind_detail); } else { - feat.setAttr("pmap:kind", "other").setZoomRange(14, 15) - .setAttrWithMinzoom("ref", shield_text, 14) - .setAttrWithMinzoom("ref_length", shield_text_length, 14) - .setAttrWithMinzoom("network", network_val, 14); - OsmNames.setOsmNames(feat, sourceFeature, 14); + feat.setAttr("pmap:kind_detail", highway); + } + + // Core OSM tags for different kinds of places + if (service != "") { + feat.setAttr("service", service); } - if (sourceFeature.hasTag("highway", "motorway_link", "trunk_link", "primary_link", "secondary_link", + if (sf.hasTag("highway", "motorway_link", "trunk_link", "primary_link", "secondary_link", "tertiary_link")) { feat.setAttr("pmap:link", 1); } - if (sourceFeature.hasTag("bridge", "yes")) { + // Set "brunnel" (bridge / tunnel) property where "level" = 1 is a bridge, 0 is ground level, and -1 is a tunnel + // Because of MapLibre performance and draw order limitations, generally the boolean is sufficent + // See also: "layer" for more complicated ±6 layering for more sophisticated graphics libraries + if (sf.hasTag("bridge") && !sf.hasTag("bridge", "no")) { feat.setAttrWithMinzoom("pmap:level", 1, 12); - } else if (sourceFeature.hasTag("tunnel", "yes")) { + } else if (sf.hasTag("tunnel") && !sf.hasTag("tunnel", "no")) { feat.setAttrWithMinzoom("pmap:level", -1, 12); } else { feat.setAttrWithMinzoom("pmap:level", 0, 12); } + + OsmNames.setOsmNames(feat, sf, min_zoom_names); } } @Override public List postProcess(int zoom, List items) throws GeometryException { - // limit the application of LinkSimplify to where cloverleafs are unlikely to be at tile edges. // TODO: selectively apply each class depending on zoom level. if (zoom < 12) { diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Transit.java b/tiles/src/main/java/com/protomaps/basemap/layers/Transit.java index 64038ad7..7ab0efa4 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Transit.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Transit.java @@ -27,40 +27,94 @@ public void processFeature(SourceFeature sf, FeatureCollector features) { int minzoom = 11; - if (sf.hasTag("service", "yard", "siding", "crossover")) { + if (sf.hasTag("aeroway", "runway")) { + minzoom = 9; + } else if (sf.hasTag("aeroway", "taxiway")) { + minzoom = 10; + } else if (sf.hasTag("service", "yard", "siding", "crossover")) { + minzoom = 13; + } else if (sf.hasTag("man_made", "pier")) { minzoom = 13; } - var feature = features.line(this.name()) - .setId(FeatureId.create(sf)) - .setAttr("railway", sf.getString("railway")) - .setAttr("route", sf.getString("route")) - .setAttr("aeroway", sf.getString("aeroway")) - .setAttr("man_made", sf.getString("pier")) - .setAttr("service", sf.getString("service")) - .setAttr("aerialway", sf.getString("aerialway")) - .setAttr("network", sf.getString("network")) - .setAttr("ref", sf.getString("ref")) - .setAttr("highspeed", sf.getString("highspeed")) - .setAttr("layer", sf.getString("layer")) - .setZoomRange(minzoom, 15); - String kind = "other"; + String kind_detail = ""; if (sf.hasTag("aeroway")) { kind = "aeroway"; + kind_detail = sf.getString("aeroway"); + } else if (sf.hasTag("railway", "disused", "funicular", "light_rail", "miniature", "monorail", "narrow_gauge", + "preserved", "subway", "tram")) { + kind = "rail"; + kind_detail = sf.getString("railway"); + minzoom = 14; + + if (sf.hasTag("railway", "disused")) { + minzoom = 15; + } } else if (sf.hasTag("railway")) { - kind = "railway"; + kind = "rail"; + kind_detail = sf.getString("railway"); + + if (kind_detail.equals("service")) { + minzoom = 13; + + // eg a rail yard + if (sf.hasTag("service")) { + minzoom = 14; + } + } } else if (sf.hasTag("ferry")) { kind = "ferry"; + kind_detail = sf.getString("ferry"); } else if (sf.hasTag("man_made", "pier")) { kind = "pier"; } else if (sf.hasTag("aerialway")) { kind = "aerialway"; + kind_detail = sf.getString("aerialway"); } - feature.setAttr("pmap:kind", kind); + var feature = features.line(this.name()) + .setId(FeatureId.create(sf)) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // Core OSM tags for different kinds of places + .setAttr("layer", sf.getString("layer")) + .setAttr("network", sf.getString("network")) + .setAttr("ref", sf.getString("ref")) + .setAttr("route", sf.getString("route")) + .setAttr("service", sf.getString("service")) + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail + .setAttr("aerialway", sf.getString("aerialway")) + .setAttr("aeroway", sf.getString("aeroway")) + .setAttr("highspeed", sf.getString("highspeed")) + .setAttr("man_made", sf.getString("pier")) + .setAttr("railway", sf.getString("railway")) + .setZoomRange(minzoom, 15); + + // Core Tilezen schema properties + if (kind_detail != "") { + feature.setAttr("pmap:kind_detail", kind_detail); + } + + // Set "brunnel" (bridge / tunnel) property where "level" = 1 is a bridge, 0 is ground level, and -1 is a tunnel + // Because of MapLibre performance and draw order limitations, generally the boolean is sufficent + // See also: "layer" for more complicated ±6 layering for more sophisticated graphics libraries + if (sf.hasTag("bridge") && !sf.hasTag("bridge", "no")) { + feature.setAttrWithMinzoom("pmap:level", 1, 12); + } else if (sf.hasTag("tunnel") && !sf.hasTag("tunnel", "no")) { + feature.setAttrWithMinzoom("pmap:level", -1, 12); + } else { + feature.setAttrWithMinzoom("pmap:level", 0, 12); + } + + // Too many small pier lines otherwise + if (kind == "pier") { + feature.setMinPixelSize(2); + } - OsmNames.setOsmNames(feature, sf, 0); + // TODO: (nvkelso 20230623) This should be variable, but 12 is better than 0 for line merging + OsmNames.setOsmNames(feature, sf, 12); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/layers/Water.java b/tiles/src/main/java/com/protomaps/basemap/layers/Water.java index 2bc2ee62..2388eefc 100644 --- a/tiles/src/main/java/com/protomaps/basemap/layers/Water.java +++ b/tiles/src/main/java/com/protomaps/basemap/layers/Water.java @@ -6,7 +6,6 @@ import com.onthegomap.planetiler.VectorTile; import com.onthegomap.planetiler.geo.GeometryException; import com.onthegomap.planetiler.reader.SourceFeature; -import com.protomaps.basemap.names.OsmNames; import com.protomaps.basemap.postprocess.Area; import java.util.List; @@ -19,57 +18,171 @@ public String name() { public void processOsm(SourceFeature sf, FeatureCollector features) { features.polygon(this.name()) + .setAttr("pmap:kind", "water") .setZoomRange(6, 15).setBufferPixels(8); } public void processNe(SourceFeature sf, FeatureCollector features) { var sourceLayer = sf.getSourceLayer(); - if (sourceLayer.equals("ne_110m_ocean") || sourceLayer.equals("ne_110m_lakes")) { - features.polygon(this.name()).setZoomRange(0, 1); - } else if (sourceLayer.equals("ne_50m_ocean") || sourceLayer.equals("ne_50m_lakes")) { - features.polygon(this.name()).setZoomRange(2, 4); - } else if (sourceLayer.equals("ne_10m_ocean") || sourceLayer.equals("ne_10m_lakes")) { - features.polygon(this.name()).setZoomRange(5, 5); + var kind = ""; + var alkaline = 0; + var reservoir = 0; + var theme_min_zoom = 0; + var theme_max_zoom = 0; + + // Only process certain Natural Earth layers + // Notably the landscan derived urban areas and NA roads supplement themes causes problems otherwise + if (sourceLayer.equals("ne_110m_ocean") || sourceLayer.equals("ne_110m_lakes") || + sourceLayer.equals("ne_50m_ocean") || sourceLayer.equals("ne_50m_lakes") || sourceLayer.equals("ne_10m_ocean") || + sourceLayer.equals("ne_10m_lakes")) { + if (sourceLayer.equals("ne_110m_ocean")) { + theme_min_zoom = 0; + theme_max_zoom = 1; + } else if (sourceLayer.equals("ne_110m_lakes")) { + theme_min_zoom = 0; + theme_max_zoom = 1; + } else if (sourceLayer.equals("ne_50m_ocean")) { + theme_min_zoom = 2; + theme_max_zoom = 4; + } else if (sourceLayer.equals("ne_50m_lakes")) { + theme_min_zoom = 2; + theme_max_zoom = 4; + } else if (sourceLayer.equals("ne_10m_ocean")) { + theme_min_zoom = 5; + theme_max_zoom = 5; + } else if (sourceLayer.equals("ne_10m_lakes")) { + theme_min_zoom = 5; + theme_max_zoom = 5; + } + + switch (sf.getString("featurecla")) { + case "Alkaline Lake" -> { + kind = "lake"; + alkaline = 1; + } + case "Lake" -> kind = "lake"; + case "Reservoir" -> { + kind = "lake"; + reservoir = 1; + } + case "Playa" -> kind = "playa"; + case "Ocean" -> kind = "ocean"; + } + + if (kind != "" && sf.hasTag("min_zoom")) { + var feature = features.polygon(this.name()) + .setAttr("pmap:kind", kind) + .setAttr("pmap:min_zoom", sf.getLong("min_zoom")) + .setZoomRange( + sf.getString("min_zoom") == null ? theme_min_zoom : (int) Double.parseDouble(sf.getString("min_zoom")), + theme_max_zoom) + .setMinPixelSize(3.0) + .setBufferPixels(8); + } } } @Override public void processFeature(SourceFeature sf, FeatureCollector features) { - if (sf.canBePolygon() && (sf.hasTag("water") || sf.hasTag("waterway") || sf.hasTag("natural", "water") || - sf.hasTag("landuse", "reservoir") || sf.hasTag("leisure", "swimming_pool"))) { + if (sf.canBePolygon() && (sf.hasTag("water") || + sf.hasTag("waterway") || + sf.hasTag("natural", "water") || + sf.hasTag("landuse", "reservoir") || + sf.hasTag("leisure", "swimming_pool"))) { + String kind = "other"; + String kind_detail = ""; + var reservoir = false; + var alkaline = false; + + // coallese values across tags to single kind value + if (sf.hasTag("natural", "water", "bay", "strait", "fjord")) { + kind = sf.getString("natural"); + if (sf.hasTag("water", "basin", "canal", "ditch", "drain", "lake", "river", "stream")) { + kind_detail = sf.getString("water"); + + // This is a bug in Tilezen v1.9 that should be fixed in 2.0 + // But isn't present in Protomaps v2 so let's fix it preemtively + if (kind_detail == "lake") { + kind = "lake"; + } + + if (sf.hasTag("water", "lagoon", "oxbow", "pond", "reservoir", "wastewater")) { + kind_detail = "lake"; + } + if (sf.hasTag("water", "reservoir")) { + reservoir = true; + } + if (sf.hasTag("water", "lagoon", "salt", "salt_pool")) { + alkaline = true; + } + } + } else if (sf.hasTag("waterway", "riverbank", "dock", "canal", "river", "stream", "ditch", "drain")) { + kind = "water"; + kind_detail = sf.getString("waterway"); + } else if (sf.hasTag("landuse", "basin")) { + kind = sf.getString("landuse"); + } else if (sf.hasTag("landuse", "reservoir")) { + kind = "water"; + kind_detail = sf.getString("landuse"); + reservoir = true; + } else if (sf.hasTag("leisure", "swimming_pool")) { + kind = "swimming_pool"; + } else if (sf.hasTag("amenity", "swimming_pool")) { + kind = "swimming_pool"; + } + var feature = features.polygon(this.name()) + // Core Tilezen schema properties + .setAttr("pmap:kind", kind) + // Core OSM tags for different kinds of places + // Add less common attributes only at higher zooms + .setAttrWithMinzoom("bridge", sf.getString("bridge"), 12) + .setAttrWithMinzoom("tunnel", sf.getString("tunnel"), 12) + .setAttrWithMinzoom("layer", sf.getString("layer"), 12) + // DEPRECATION WARNING: Marked for deprecation in v4 schema, do not use these for styling + // If an explicate value is needed it should bea kind, or included in kind_detail .setAttr("natural", sf.getString("natural")) .setAttr("landuse", sf.getString("landuse")) .setAttr("leisure", sf.getString("leisure")) .setAttr("water", sf.getString("water")) .setAttr("waterway", sf.getString("waterway")) - // Add less common attributes only at higher zooms - .setAttrWithMinzoom("bridge", sf.getString("bridge"), 12) - .setAttrWithMinzoom("tunnel", sf.getString("tunnel"), 12) - .setAttrWithMinzoom("layer", sf.getString("layer"), 12) .setZoomRange(6, 15) .setMinPixelSize(3.0) .setBufferPixels(8); + // Core Tilezen schema properties + if (kind_detail != "") { + feature.setAttr("pmap:kind_detail", kind_detail); + } + if (sf.hasTag("water", "reservoir") || reservoir) { + feature.setAttr("reservoir", true); + } + if (sf.hasTag("water", "lagoon", "salt", "salt_pool") || alkaline) { + feature.setAttr("alkaline", true); + } if (sf.hasTag("intermittent", "yes")) { - feature.setAttr("intermittent", 1); + feature.setAttr("intermittent", true); } - OsmNames.setOsmNames(feature, sf, 0); + // NOTE: water labels for polygons are found in the physical_point layer (see also physical_line layer) + //OsmNames.setOsmNames(feature, sf, 0); } } @Override public List postProcess(int zoom, List items) throws GeometryException { items = Area.addAreaTag(items); + if (zoom == 15) return items; + int minArea = 400 / (4096 * 4096) * (256 * 256); if (zoom == 6) minArea = 600 / (4096 * 4096) * (256 * 256); else if (zoom <= 5) minArea = 800 / (4096 * 4096) * (256 * 256); items = Area.filterArea(items, minArea); + return FeatureMerge.mergeOverlappingPolygons(items, 1); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/names/NeNames.java b/tiles/src/main/java/com/protomaps/basemap/names/NeNames.java index 6faa92fd..97024f86 100644 --- a/tiles/src/main/java/com/protomaps/basemap/names/NeNames.java +++ b/tiles/src/main/java/com/protomaps/basemap/names/NeNames.java @@ -5,14 +5,14 @@ import java.util.Map; public class NeNames { - public static FeatureCollector.Feature setNeNames(FeatureCollector.Feature feature, SourceFeature source, + public static FeatureCollector.Feature setNeNames(FeatureCollector.Feature feature, SourceFeature sf, int minzoom) { - for (Map.Entry tag : source.tags().entrySet()) { + for (Map.Entry tag : sf.tags().entrySet()) { var key = tag.getKey(); if (key.equals("name")) { - feature.setAttrWithMinzoom(key, source.getTag(key), minzoom); + feature.setAttrWithMinzoom(key, sf.getTag(key), minzoom); } else if (key.startsWith("name_")) { - feature.setAttrWithMinzoom(key.replace("_", ":"), source.getTag(key), minzoom); + feature.setAttrWithMinzoom(key.replace("_", ":"), sf.getTag(key), minzoom); } } diff --git a/tiles/src/main/java/com/protomaps/basemap/names/OsmNames.java b/tiles/src/main/java/com/protomaps/basemap/names/OsmNames.java index f46c2991..87deab50 100644 --- a/tiles/src/main/java/com/protomaps/basemap/names/OsmNames.java +++ b/tiles/src/main/java/com/protomaps/basemap/names/OsmNames.java @@ -5,12 +5,12 @@ import java.util.Map; public class OsmNames { - public static FeatureCollector.Feature setOsmNames(FeatureCollector.Feature feature, SourceFeature source, + public static FeatureCollector.Feature setOsmNames(FeatureCollector.Feature feature, SourceFeature sf, int minzoom) { - for (Map.Entry tag : source.tags().entrySet()) { + for (Map.Entry tag : sf.tags().entrySet()) { var key = tag.getKey(); if (key.equals("name") || key.startsWith("name:")) { - feature.setAttrWithMinzoom(key, source.getTag(key), minzoom); + feature.setAttrWithMinzoom(key, sf.getTag(key), minzoom); } }