diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 216ac2039..41a359531 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -269,6 +269,12 @@ The MUTCD’s standard colors are designed for high-contrast backgrounds and leg
See the [developer tools](dev/README.md) for an importable, Inkscape-compatible palette file.
+### Icon Grid Alignment
+
+There is a utility script called icon_grid that will generate a pixel grid on an SVG. This can be used to check how well the icon will align to the pixel grid. Run this utility as follows:
+
+`npm run icon_grid -- icons/poi_fuel.svg`
+
### Font Sizes
Shields should target 8-14px text actual-size character heights for readability:
diff --git a/dev/README.md b/dev/README.md
index 1b7c23781..5c81836a3 100644
--- a/dev/README.md
+++ b/dev/README.md
@@ -19,4 +19,4 @@ Map sample images can be generated with a script. See [sample_locations.json](te
1. Create a JSON file with the map location and clipping rectangles
2. Start the server in the background with `npm start &`
3. Configure playwright: either `npx playwright install chromium` or `export CHROME_BIN=/usr/bin/chromium`
-4. Run the generate_samples script and pass the JSON file location: `npm run generate_samples -- test/sample_locations.json``
+4. Run the generate_samples script and pass the JSON file location: `npm run generate_samples -- chrome test/sample_locations.json``. You may also use "firefox" or "safari".
diff --git a/icons/poi_book_upright.svg b/icons/poi_book_upright.svg
new file mode 100644
index 000000000..b13bdc7bc
--- /dev/null
+++ b/icons/poi_book_upright.svg
@@ -0,0 +1,6 @@
+
diff --git a/icons/poi_car_repair.svg b/icons/poi_car_repair.svg
new file mode 100644
index 000000000..042bff803
--- /dev/null
+++ b/icons/poi_car_repair.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/poi_car_shop.svg b/icons/poi_car_shop.svg
new file mode 100644
index 000000000..cfcf82ba7
--- /dev/null
+++ b/icons/poi_car_shop.svg
@@ -0,0 +1,4 @@
+
diff --git a/icons/poi_envelope.svg b/icons/poi_envelope.svg
new file mode 100644
index 000000000..65113dabc
--- /dev/null
+++ b/icons/poi_envelope.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/poi_hostel.svg b/icons/poi_hostel.svg
new file mode 100644
index 000000000..04d8bf388
--- /dev/null
+++ b/icons/poi_hostel.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/poi_hotel.svg b/icons/poi_hotel.svg
new file mode 100644
index 000000000..576f5a1d7
--- /dev/null
+++ b/icons/poi_hotel.svg
@@ -0,0 +1,3 @@
+
diff --git a/icons/poi_taxi.svg b/icons/poi_taxi.svg
new file mode 100644
index 000000000..c8dcb2193
--- /dev/null
+++ b/icons/poi_taxi.svg
@@ -0,0 +1,5 @@
+
diff --git a/icons/shield_ca_ns_s_bdolsd.svg b/icons/shield_ca_ns_s_bdolsd.svg
new file mode 100644
index 000000000..fddb5e67c
--- /dev/null
+++ b/icons/shield_ca_ns_s_bdolsd.svg
@@ -0,0 +1,20 @@
+
diff --git a/icons/shield_ca_ns_s_cet.svg b/icons/shield_ca_ns_s_cet.svg
new file mode 100644
index 000000000..f8523af35
--- /dev/null
+++ b/icons/shield_ca_ns_s_cet.svg
@@ -0,0 +1,21 @@
+
diff --git a/icons/shield_ca_ns_s_ct.svg b/icons/shield_ca_ns_s_ct.svg
new file mode 100644
index 000000000..fa2c05e5b
--- /dev/null
+++ b/icons/shield_ca_ns_s_ct.svg
@@ -0,0 +1,16 @@
+
diff --git a/icons/shield_ca_ns_s_dnisd.svg b/icons/shield_ca_ns_s_dnisd.svg
new file mode 100644
index 000000000..b46081a94
--- /dev/null
+++ b/icons/shield_ca_ns_s_dnisd.svg
@@ -0,0 +1,12 @@
+
diff --git a/icons/shield_ca_ns_s_et.svg b/icons/shield_ca_ns_s_et.svg
new file mode 100644
index 000000000..23ec82fc3
--- /dev/null
+++ b/icons/shield_ca_ns_s_et.svg
@@ -0,0 +1,16 @@
+
diff --git a/icons/shield_ca_ns_s_fdlt.svg b/icons/shield_ca_ns_s_fdlt.svg
new file mode 100644
index 000000000..8f057fcc1
--- /dev/null
+++ b/icons/shield_ca_ns_s_fdlt.svg
@@ -0,0 +1,25 @@
+
diff --git a/icons/shield_ca_ns_s_gt.svg b/icons/shield_ca_ns_s_gt.svg
new file mode 100644
index 000000000..b907a78cf
--- /dev/null
+++ b/icons/shield_ca_ns_s_gt.svg
@@ -0,0 +1,17 @@
+
diff --git a/icons/shield_ca_ns_s_ksd.svg b/icons/shield_ca_ns_s_ksd.svg
new file mode 100644
index 000000000..5270f0e63
--- /dev/null
+++ b/icons/shield_ca_ns_s_ksd.svg
@@ -0,0 +1,19 @@
+
diff --git a/icons/shield_ca_ns_s_lr.svg b/icons/shield_ca_ns_s_lr.svg
new file mode 100644
index 000000000..a478d5625
--- /dev/null
+++ b/icons/shield_ca_ns_s_lr.svg
@@ -0,0 +1,16 @@
+
diff --git a/icons/shield_ca_ns_s_md.svg b/icons/shield_ca_ns_s_md.svg
new file mode 100644
index 000000000..5e35b59b6
--- /dev/null
+++ b/icons/shield_ca_ns_s_md.svg
@@ -0,0 +1,20 @@
+
diff --git a/icons/shield_ca_ns_s_mt.svg b/icons/shield_ca_ns_s_mt.svg
new file mode 100644
index 000000000..2c36efcf6
--- /dev/null
+++ b/icons/shield_ca_ns_s_mt.svg
@@ -0,0 +1,19 @@
+
diff --git a/icons/shield_ca_ns_s_st.svg b/icons/shield_ca_ns_s_st.svg
new file mode 100644
index 000000000..46ee7ebe1
--- /dev/null
+++ b/icons/shield_ca_ns_s_st.svg
@@ -0,0 +1,15 @@
+
diff --git a/icons/shield_us_az_scenic.svg b/icons/shield_us_az_scenic.svg
new file mode 100644
index 000000000..d027e8acd
--- /dev/null
+++ b/icons/shield_us_az_scenic.svg
@@ -0,0 +1,16 @@
+
diff --git a/package-lock.json b/package-lock.json
index 7833bdf99..623957d73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
"open": "^8.4.2",
"pbf": "^3.2.1",
"prettier": "^2.3.2",
+ "sharp": "^0.33.2",
"shx": "^0.3.4",
"svgo": "^2.8.0",
"tsx": "^4.6.2",
@@ -74,6 +75,91 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/@basemaps/sprites/node_modules/decompress-response": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+ "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+ "dev": true,
+ "dependencies": {
+ "mimic-response": "^3.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@basemaps/sprites/node_modules/mimic-response": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+ "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@basemaps/sprites/node_modules/sharp": {
+ "version": "0.32.6",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
+ "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "dev": true,
+ "hasInstallScript": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "node-addon-api": "^6.1.0",
+ "prebuild-install": "^7.1.1",
+ "semver": "^7.5.4",
+ "simple-get": "^4.0.1",
+ "tar-fs": "^3.0.4",
+ "tunnel-agent": "^0.6.0"
+ },
+ "engines": {
+ "node": ">=14.15.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@basemaps/sprites/node_modules/simple-get": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+ "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "dependencies": {
+ "decompress-response": "^6.0.0",
+ "once": "^1.3.1",
+ "simple-concat": "^1.0.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
+ "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@esbuild/android-arm": {
"version": "0.17.19",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz",
@@ -426,6 +512,456 @@
"node": ">=12"
}
},
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz",
+ "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz",
+ "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "macos": ">=11",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "macos": ">=10.13",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz",
+ "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz",
+ "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz",
+ "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz",
+ "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz",
+ "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz",
+ "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz",
+ "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz",
+ "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz",
+ "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz",
+ "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz",
+ "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz",
+ "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz",
+ "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^0.45.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz",
+ "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz",
+ "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -1990,10 +2526,13 @@
}
},
"node_modules/function-bind": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
- "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==",
- "dev": true
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
},
"node_modules/function.prototype.name": {
"version": "1.1.5",
@@ -3273,9 +3812,9 @@
"dev": true
},
"node_modules/node-abi": {
- "version": "3.51.0",
- "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.51.0.tgz",
- "integrity": "sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==",
+ "version": "3.54.0",
+ "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.54.0.tgz",
+ "integrity": "sha512-p7eGEiQil0YUV3ItH4/tBb781L5impVmmx2E9FRKF7d18XXzp4PGT2tdYMFY6wQqgxD0IwNZOiSJ0/K0fSi/OA==",
"dev": true,
"dependencies": {
"semver": "^7.3.5"
@@ -4348,78 +4887,43 @@
"dev": true
},
"node_modules/sharp": {
- "version": "0.32.6",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
- "integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
+ "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.2",
- "node-addon-api": "^6.1.0",
- "prebuild-install": "^7.1.1",
- "semver": "^7.5.4",
- "simple-get": "^4.0.1",
- "tar-fs": "^3.0.4",
- "tunnel-agent": "^0.6.0"
+ "semver": "^7.5.4"
},
"engines": {
- "node": ">=14.15.0"
+ "libvips": ">=8.15.1",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
- }
- },
- "node_modules/sharp/node_modules/decompress-response": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
- "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
- "dev": true,
- "dependencies": {
- "mimic-response": "^3.1.0"
},
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/sharp/node_modules/mimic-response": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
- "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
- "dev": true,
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/sharp/node_modules/simple-get": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
- "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
- "dev": true,
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "dependencies": {
- "decompress-response": "^6.0.0",
- "once": "^1.3.1",
- "simple-concat": "^1.0.0"
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.2",
+ "@img/sharp-darwin-x64": "0.33.2",
+ "@img/sharp-libvips-darwin-arm64": "1.0.1",
+ "@img/sharp-libvips-darwin-x64": "1.0.1",
+ "@img/sharp-libvips-linux-arm": "1.0.1",
+ "@img/sharp-libvips-linux-arm64": "1.0.1",
+ "@img/sharp-libvips-linux-s390x": "1.0.1",
+ "@img/sharp-libvips-linux-x64": "1.0.1",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.1",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.1",
+ "@img/sharp-linux-arm": "0.33.2",
+ "@img/sharp-linux-arm64": "0.33.2",
+ "@img/sharp-linux-s390x": "0.33.2",
+ "@img/sharp-linux-x64": "0.33.2",
+ "@img/sharp-linuxmusl-arm64": "0.33.2",
+ "@img/sharp-linuxmusl-x64": "0.33.2",
+ "@img/sharp-wasm32": "0.33.2",
+ "@img/sharp-win32-ia32": "0.33.2",
+ "@img/sharp-win32-x64": "0.33.2"
}
},
"node_modules/shebang-command": {
@@ -4703,9 +5207,9 @@
"dev": true
},
"node_modules/streamx": {
- "version": "2.15.5",
- "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.5.tgz",
- "integrity": "sha512-9thPGMkKC2GctCzyCUjME3yR03x2xNo0GPKGkRw2UMYN+gqWa9uqpyNWhmsNCutU5zHmkUum0LsCRQTXUgUCAg==",
+ "version": "2.15.6",
+ "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.15.6.tgz",
+ "integrity": "sha512-q+vQL4AAz+FdfT137VF69Cc/APqUbxy+MDOImRrMvchJpigHj9GksgDU2LYbO9rx7RX6osWgxJB2WxhYv4SZAw==",
"dev": true,
"dependencies": {
"fast-fifo": "^1.1.0",
@@ -5003,6 +5507,13 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "dev": true,
+ "optional": true
+ },
"node_modules/tsx": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.6.2.tgz",
diff --git a/package.json b/package.json
index ecef53ce5..a55d536da 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"code_format:svgo": "svgo -q -f icons/",
"extract_layer": "node scripts/extract_layer",
"generate_samples": "tsx scripts/generate_samples.ts",
+ "icon_grid": "tsx scripts/icon_grid.ts",
"presprites": "shx rm -rf dist/sprites",
"serve": "tsx scripts/serve",
"shields": "node scripts/generate_shield_defs.js -o dist/shields.json",
@@ -69,6 +70,7 @@
"open": "^8.4.2",
"pbf": "^3.2.1",
"prettier": "^2.3.2",
+ "sharp": "^0.33.2",
"shx": "^0.3.4",
"svgo": "^2.8.0",
"tsx": "^4.6.2",
diff --git a/scripts/generate_samples.ts b/scripts/generate_samples.ts
index 3093bba0f..2d0c6f5ce 100644
--- a/scripts/generate_samples.ts
+++ b/scripts/generate_samples.ts
@@ -1,5 +1,11 @@
import fs from "node:fs";
-import { chromium } from "@playwright/test";
+import {
+ Browser,
+ BrowserContext,
+ chromium,
+ firefox,
+ webkit,
+} from "@playwright/test";
import type * as maplibre from "maplibre-gl";
// Declare a global augmentation for the Window interface
@@ -33,23 +39,37 @@ const loadSampleLocations = (filePath: string): SampleSpecification[] => {
const sampleFolder = "./samples";
-const jsonSampleLocations = process.argv[2] ?? "test/sample_locations.json";
+const jsonSampleLocations = process.argv[3] ?? "test/sample_locations.json";
console.log(`Loading sample locations from ${jsonSampleLocations}`);
+const browserType = process.argv[2] ?? "chrome";
+console.log(`Using browser type: ${browserType}`);
+
const screenshots: SampleSpecification[] =
loadSampleLocations(jsonSampleLocations);
fs.mkdirSync(sampleFolder, { recursive: true });
-const browser = await chromium.launch({
- executablePath: process.env.CHROME_BIN,
- args: ["--headless=new"],
-});
-const context = await browser.newContext({
+let browser: Browser;
+
+switch (browserType) {
+ case "chrome":
+ default:
+ browser = await chromium.launch({
+ executablePath: process.env.CHROME_BIN,
+ args: ["--headless=new"],
+ });
+ break;
+ case "firefox":
+ browser = await firefox.launch();
+ break;
+ case "safari":
+ browser = await webkit.launch();
+}
+
+const context: BrowserContext = await browser.newContext({
bypassCSP: true,
- userAgent:
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36",
});
const page = await context.newPage();
diff --git a/scripts/icon_grid.ts b/scripts/icon_grid.ts
new file mode 100644
index 000000000..72996e16b
--- /dev/null
+++ b/scripts/icon_grid.ts
@@ -0,0 +1,73 @@
+import sharp from "sharp";
+import fs from "fs";
+import path from "path";
+
+const svgFilename = process.argv[2];
+const outputFilenameBase = path.parse(svgFilename).name;
+const outputFilename = `${outputFilenameBase}_preview.png`;
+const scale = 40;
+
+// Function to generate a pixel grid
+function generateGridPattern(xOffset: number, yOffset: number): Buffer {
+ const gridSvg = ``;
+ return Buffer.from(gridSvg);
+}
+
+// Convert and scale SVG
+async function convertAndScaleSVG(svgFilename: string): Promise {
+ const svgBuffer = fs.readFileSync(svgFilename);
+
+ // Get dimensions of the original SVG
+ const metadata = await sharp(svgBuffer).metadata();
+ const width = metadata.width! * scale;
+ const height = metadata.height! * scale;
+
+ // Resize the SVG
+ const resizedSvgBuffer = await sharp(svgBuffer, {
+ density: 72 * scale,
+ })
+ .resize(width, height)
+ .toBuffer();
+
+ const xOffset: number = metadata.width! % 2 == 0 ? scale / 2 : 0;
+ const yOffset: number = metadata.height! % 2 == 0 ? scale / 2 : 0;
+
+ // Generate a pixel grid pattern
+ const gridPattern = generateGridPattern(xOffset, yOffset);
+
+ // Composite the scaled image over the grid
+ await sharp({
+ create: {
+ width,
+ height,
+ channels: 4,
+ background: { r: 211, g: 211, b: 211, alpha: 1 }, // Light gray to show white borders
+ },
+ })
+ .composite([
+ { input: resizedSvgBuffer, blend: "over" },
+ { input: gridPattern, tile: true, blend: "over" },
+ ])
+ .toFile(outputFilename);
+ console.log(`Wrote ${outputFilename}`);
+}
+
+if (!svgFilename) {
+ console.error("Please provide an SVG filename.");
+ process.exit(1);
+}
+
+try {
+ await convertAndScaleSVG(svgFilename);
+} catch (error) {
+ console.error("Error: ", error);
+ process.exit(1);
+}
diff --git a/scripts/taginfo_template.json b/scripts/taginfo_template.json
index 868967ced..672adcccb 100644
--- a/scripts/taginfo_template.json
+++ b/scripts/taginfo_template.json
@@ -399,6 +399,14 @@
"doc_url": "https://openmaptiles.org/schema/#poi",
"icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_school.svg"
},
+ {
+ "key": "amenity",
+ "value": "taxi",
+ "object_types": ["node", "area"],
+ "description": "Taxi stands are marked by an icon representing the front view of a taxi cab.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_taxi.svg"
+ },
{
"key": "amenity",
"value": "kindergarten",
@@ -479,6 +487,46 @@
"doc_url": "https://openmaptiles.org/schema/#poi",
"icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_town_hall.svg"
},
+ {
+ "key": "amenity",
+ "value": "post_office",
+ "object_types": ["node", "area"],
+ "description": "Post offices are marked by an icon representing an envelope.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_envelope.svg"
+ },
+ {
+ "key": "tourism",
+ "value": "guest_house",
+ "object_types": ["node", "area"],
+ "description": "Guest houses are marked by an icon representing a person laying in a bed.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hotel.svg"
+ },
+ {
+ "key": "tourism",
+ "value": "hostel",
+ "object_types": ["node", "area"],
+ "description": "Hostels are marked by an icon representing two people laying in a bunk bed.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hostel.svg"
+ },
+ {
+ "key": "tourism",
+ "value": "hotel",
+ "object_types": ["node", "area"],
+ "description": "Hotels are marked by an icon representing a person laying in a bed.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hotel.svg"
+ },
+ {
+ "key": "tourism",
+ "value": "motel",
+ "object_types": ["node", "area"],
+ "description": "Motels are marked by an icon representing a person laying in a bed.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_hotel.svg"
+ },
{
"key": "tourism",
"value": "museum",
@@ -487,7 +535,6 @@
"doc_url": "https://openmaptiles.org/schema/#poi",
"icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_museum.svg"
},
-
{
"key": "amenity",
"value": "clinic",
@@ -496,6 +543,14 @@
"doc_url": "https://openmaptiles.org/schema/#poi",
"icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_health_cross.svg"
},
+ {
+ "key": "amenity",
+ "value": "library",
+ "object_types": ["node", "area"],
+ "description": "Libraries are marked by an icon representing a book.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_book_upright.svg"
+ },
{
"key": "amenity",
"value": "parking",
@@ -632,6 +687,30 @@
"doc_url": "https://openmaptiles.org/schema/#poi",
"icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_pow_taoist.svg"
},
+ {
+ "key": "shop",
+ "value": "books",
+ "object_types": ["node", "area"],
+ "description": "Bookstores are marked by an icon representing a book.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_book_upright.svg"
+ },
+ {
+ "key": "shop",
+ "value": "car",
+ "object_types": ["node", "area"],
+ "description": "Car dealerships are marked by an icon representing a front facing vehicle.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_car_shop.svg"
+ },
+ {
+ "key": "shop",
+ "value": "car_repair",
+ "object_types": ["node", "area"],
+ "description": "Car mechanic shops are marked by an icon representing a front facing vehicle with a wrench above it.",
+ "doc_url": "https://openmaptiles.org/schema/#poi",
+ "icon_url": "https://raw.githubusercontent.com/ZeLonewolf/openstreetmap-americana/main/icons/poi_car_repair.svg"
+ },
{
"key": "shop",
"value": "supermarket",
diff --git a/shieldlib/src/screen_gfx.ts b/shieldlib/src/screen_gfx.ts
index eec3087e8..3be40e4be 100644
--- a/shieldlib/src/screen_gfx.ts
+++ b/shieldlib/src/screen_gfx.ts
@@ -5,7 +5,9 @@ import rgba from "color-rgba";
const defaultFontFamily = '"sans-serif-condensed", "Arial Narrow", sans-serif';
export const shieldFont = (size: number, fontFamily: string) =>
`condensed 500 ${size}px ${fontFamily || defaultFontFamily}`;
-export const fontSizeThreshold = 12;
+
+//If a computed shield font size is below this value, choose a wider shield if possible
+export const fontSizeThreshold = 11.8;
// Replaces `sourceVal` with a blend of `lightenVal` and `darkenVal` proportional to the brightness;
// i.e. white becomes `darkenVal`, black becomes `lightenVal`, and anit-aliased pixels remain anit-aliased
diff --git a/shieldlib/src/shield.js b/shieldlib/src/shield.js
index f8d3c342d..8a8202cde 100644
--- a/shieldlib/src/shield.js
+++ b/shieldlib/src/shield.js
@@ -76,18 +76,6 @@ function getDrawFunc(shieldDef) {
return ShieldDraw.blank;
}
-function drawShield(r, ctx, shieldDef, routeDef) {
- let bannerCount = getBannerCount(shieldDef);
- let yOffset = bannerCount * r.px(r.options.bannerHeight);
-
- //Shift canvas to draw shield below banner
- ctx.save();
- ctx.translate(0, yOffset);
- let drawFunc = getDrawFunc(shieldDef);
- drawFunc(r, ctx, routeDef.ref);
- ctx.restore();
-}
-
function getDrawHeight(r, shieldDef) {
if (typeof shieldDef.shapeBlank != "undefined") {
return ShieldDraw.shapeHeight(r, shieldDef.shapeBlank.drawFunc);
@@ -95,30 +83,7 @@ function getDrawHeight(r, shieldDef) {
return r.shieldSize();
}
-function drawShieldText(r, ctx, shieldDef, routeDef) {
- var bannerCount = getBannerCount(shieldDef);
- var shieldBounds = null;
-
- var shieldArtwork = getRasterShieldBlank(r, shieldDef, routeDef);
- let yOffset = bannerCount * r.px(r.options.bannerHeight);
-
- if (shieldArtwork == null) {
- ctx.translate(0, yOffset);
- let drawFunc = getDrawFunc(shieldDef);
- drawFunc(r, ctx, routeDef.ref);
- ctx.translate(0, -yOffset);
-
- shieldBounds = {
- width: ctx.canvas.width,
- height: getDrawHeight(r, shieldDef),
- };
- } else {
- shieldBounds = {
- width: shieldArtwork.data.width,
- height: shieldArtwork.data.height,
- };
- }
-
+function drawShieldText(r, ctx, shieldDef, routeDef, shieldBounds) {
if (shieldDef.notext) {
//If the shield definition says not to draw a ref, ignore ref
return ctx;
@@ -132,8 +97,6 @@ function drawShieldText(r, ctx, shieldDef, routeDef) {
shieldBounds
);
- textLayout.yBaseline += bannerCount * r.px(r.options.bannerHeight);
-
if (typeof r.options.SHIELD_TEXT_HALO_COLOR_OVERRIDE !== "undefined") {
ctx.strokeStyle = options.SHIELD_TEXT_HALO_COLOR_OVERRIDE;
ShieldText.drawShieldHaloText(r, ctx, routeDef.ref, textLayout);
@@ -150,8 +113,7 @@ function drawShieldText(r, ctx, shieldDef, routeDef) {
ctx.lineWidth = r.px(1);
ctx.strokeRect(
r.px(shieldDef.padding.left - 0.5),
- bannerCount * r.px(r.options.bannerHeight) +
- r.px(shieldDef.padding.top - 0.5),
+ r.px(shieldDef.padding.top - 0.5),
shieldBounds.width -
r.px(shieldDef.padding.left + shieldDef.padding.right - 1),
shieldBounds.height -
@@ -203,6 +165,11 @@ function refForDefs(routeDef, shieldDef) {
}
function getShieldDef(shields, routeDef) {
+ if (!shields) {
+ //This occurs if the ShieldJSON is loaded from the network and hasn't loaded yet.
+ return null;
+ }
+
var shieldDef = shields[routeDef.network];
if (routeDef == null) {
@@ -294,6 +261,17 @@ function getDrawnShieldBounds(r, shieldDef, ref) {
return { width, height };
}
+function bannerAreaHeight(r, bannerCount) {
+ if (bannerCount === 0) {
+ return 0;
+ }
+ return (
+ bannerCount * r.px(r.options.bannerHeight) +
+ //No padding after last banner
+ (bannerCount - 1) * r.px(r.options.bannerPadding)
+ );
+}
+
export function generateShieldCtx(r, routeDef) {
let shieldDef = getShieldDef(r.shieldDef, routeDef);
@@ -311,18 +289,25 @@ export function generateShieldCtx(r, routeDef) {
let width = r.shieldSize();
let height = r.shieldSize();
+ let shieldBounds = null;
+
if (sourceSprite == null) {
if (typeof shieldDef.shapeBlank != "undefined") {
let bounds = getDrawnShieldBounds(r, shieldDef, routeDef.ref);
width = bounds.width;
height = bounds.height;
}
+ shieldBounds = {
+ width: width,
+ height: getDrawHeight(r, shieldDef),
+ };
} else {
width = sourceSprite.data.width;
height = sourceSprite.data.height;
+ shieldBounds = { width, height };
}
- let bannerHeight = bannerCount * r.px(r.options.bannerHeight);
+ let bannerHeight = bannerAreaHeight(r, bannerCount);
height += bannerHeight;
//Generate empty canvas sized to the graphic
@@ -338,9 +323,15 @@ export function generateShieldCtx(r, routeDef) {
// Add the halo around modifier plaque text
drawBannerHalos(r, ctx, shieldDef);
+ //Shift canvas to draw shield below banner
+ ctx.save();
+ ctx.translate(0, bannerHeight);
+
if (sourceSprite == null) {
- drawShield(r, ctx, shieldDef, routeDef);
+ let drawFunc = getDrawFunc(shieldDef);
+ drawFunc(r, ctx, routeDef.ref);
} else {
+ //This is a raw copy, so the yOffset (bannerHeight) is needed
Gfx.transposeImageData(
ctx,
sourceSprite,
@@ -352,7 +343,9 @@ export function generateShieldCtx(r, routeDef) {
}
// Draw the shield text
- drawShieldText(r, ctx, shieldDef, routeDef);
+ drawShieldText(r, ctx, shieldDef, routeDef, shieldBounds);
+
+ ctx.restore();
// Add modifier plaque text
drawBanners(r, ctx, shieldDef);
diff --git a/shieldlib/src/shield_banner.ts b/shieldlib/src/shield_banner.ts
index 6961865b5..deb86599d 100644
--- a/shieldlib/src/shield_banner.ts
+++ b/shieldlib/src/shield_banner.ts
@@ -133,7 +133,7 @@ function drawBannerTextComponent(
textComponent: boolean
): void {
const bannerPadding = {
- top: r.options.bannerPadding,
+ top: 0,
bottom: 0,
left: 0,
right: 0,
@@ -161,7 +161,7 @@ function drawBannerTextComponent(
text,
textLayout.xBaseline,
textLayout.yBaseline +
- bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding)
+ bannerIndex * r.px(r.options.bannerHeight + r.options.bannerPadding)
);
} else {
ctx.shadowBlur = 0;
@@ -170,7 +170,7 @@ function drawBannerTextComponent(
text,
textLayout.xBaseline,
textLayout.yBaseline +
- bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding)
+ bannerIndex * r.px(r.options.bannerHeight + r.options.bannerPadding)
);
ctx.shadowColor = null;
diff --git a/shieldlib/src/shield_renderer.ts b/shieldlib/src/shield_renderer.ts
index cc8c30e93..826555688 100644
--- a/shieldlib/src/shield_renderer.ts
+++ b/shieldlib/src/shield_renderer.ts
@@ -70,7 +70,7 @@ class MaplibreGLSpriteRepository implements SpriteRepository {
options: StyleImageMetadata,
update: boolean
): void {
- if (update) {
+ if (update && this.map.listImages().includes(spriteID)) {
this.map.removeImage(spriteID);
this.map.addImage(spriteID, image);
} else {
@@ -106,6 +106,7 @@ export class AbstractShieldRenderer {
this._renderContext.options = shieldSpec.options;
this._renderContext.shieldDef = shieldSpec.networks;
this._fontSpec = "1em " + shieldSpec.options.shieldFont;
+ console.log("ShieldJSON loaded");
if (this._map) {
this.reloadShieldsOnFontLoad();
}
diff --git a/shieldlib/src/shield_text.ts b/shieldlib/src/shield_text.ts
index 350f7f02c..5bc1071d0 100644
--- a/shieldlib/src/shield_text.ts
+++ b/shieldlib/src/shield_text.ts
@@ -137,6 +137,15 @@ function triangleDownTextConstraint(
};
}
+// Warning!!! Hack!!!
+function isRunningInWebKit(): boolean {
+ if (typeof window === "undefined") {
+ return false;
+ }
+ const userAgent = window.navigator.userAgent;
+ return /WebKit/i.test(userAgent) && !/Chrome/i.test(userAgent);
+}
+
/**
* Determines the position and font size to draw text so that it fits within
* a bounding box.
@@ -157,34 +166,49 @@ export function layoutShieldText(
textLayoutDef: TextLayout,
maxFontSize: number = 14
): TextPlacement {
- var padTop = r.px(padding.top) || 0;
- var padBot = r.px(padding.bottom) || 0;
- var padLeft = r.px(padding.left) || 0;
- var padRight = r.px(padding.right) || 0;
+ let padTop = r.px(padding.top) || 0;
+ let padBot = r.px(padding.bottom) || 0;
+ let padLeft = r.px(padding.left) || 0;
+ let padRight = r.px(padding.right) || 0;
- var maxFont = r.px(maxFontSize);
+ let maxFont = r.px(maxFontSize);
//Temporary canvas for text measurment
- var ctx = r.gfxFactory.createGraphics(bounds);
+ let ctx: CanvasRenderingContext2D = r.gfxFactory.createGraphics(bounds);
ctx.font = Gfx.shieldFont(Gfx.fontSizeThreshold, r.options.shieldFont);
- ctx.textAlign = "center";
+ ctx.textAlign = "left";
ctx.textBaseline = "top";
- var metrics = ctx.measureText(text);
+ let metrics: TextMetrics = ctx.measureText(text);
- var textWidth = metrics.width;
- var textHeight = metrics.actualBoundingBoxDescent;
+ let textWidth: number =
+ Math.abs(metrics.actualBoundingBoxLeft) +
+ Math.abs(metrics.actualBoundingBoxRight);
+ let textHeight: number =
+ Math.abs(metrics.actualBoundingBoxDescent) +
+ Math.abs(metrics.actualBoundingBoxAscent);
- var availHeight = bounds.height - padTop - padBot;
- var availWidth = bounds.width - padLeft - padRight;
+ //Adjust for excess descender text height across browsers
+ textHeight *= 0.9;
- var xBaseline = padLeft + availWidth / 2;
+ //Adjust for excess text height measured in Webkit engine specifically
+ if (isRunningInWebKit()) {
+ textHeight *= 0.54;
+ }
+
+ let availHeight: number = bounds.height - padTop - padBot;
+ let availWidth: number = bounds.width - padLeft - padRight;
+
+ let xBaseline: number = padLeft + availWidth / 2;
let textLayoutFunc = drawTextFunctions[textLayoutDef.constraintFunc];
- let textConstraint = textLayoutFunc(
- { height: availHeight, width: availWidth },
- { height: textHeight, width: textWidth },
+ let spaceAvail: Dimension = { height: availHeight, width: availWidth };
+ let measuredTextBounds: Dimension = { height: textHeight, width: textWidth };
+
+ let textConstraint: TextTransform = textLayoutFunc(
+ spaceAvail,
+ measuredTextBounds,
textLayoutDef.options
);
@@ -195,20 +219,23 @@ export function layoutShieldText(
);
ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont);
- ctx.textAlign = "center";
+ ctx.textAlign = "left";
ctx.textBaseline = "top";
metrics = ctx.measureText(text);
- textHeight = metrics.actualBoundingBoxDescent;
+ textHeight =
+ Math.abs(metrics.actualBoundingBoxDescent) +
+ Math.abs(metrics.actualBoundingBoxAscent);
let yBaseline: number;
switch (textConstraint.valign) {
case VerticalAlignment.Top:
- yBaseline = padTop;
+ yBaseline = padTop + metrics.actualBoundingBoxAscent;
break;
case VerticalAlignment.Bottom:
- yBaseline = padTop + availHeight - textHeight;
+ yBaseline =
+ padTop + availHeight - textHeight + metrics.actualBoundingBoxAscent;
break;
case VerticalAlignment.Middle:
default:
diff --git a/src/configs/config.aws.js b/src/configs/config.aws.js
index 9727e7f64..51e032ebc 100644
--- a/src/configs/config.aws.js
+++ b/src/configs/config.aws.js
@@ -3,7 +3,7 @@
/*
Planetiler tile server, hosted at AWS
*/
-const OPENMAPTILES_URL = "https://tile.ourmap.us/data/v3.json";
+const OPENMAPTILES_URL = "https://tile.ourmap.us/data/omt_3_15.json";
/*
The following two variables override the color of the bounding box and halo of
diff --git a/src/js/shield_defs.js b/src/js/shield_defs.js
index a906aeb39..03f6c1bd9 100644
--- a/src/js/shield_defs.js
+++ b/src/js/shield_defs.js
@@ -204,6 +204,9 @@ export function loadShields() {
Color.shields.brown,
Color.shields.white
);
+ shields["CA:NS:S"] = {
+ notext: true,
+ };
// Northwest Territories
shields["CA:NT"] = {
@@ -653,6 +656,12 @@ export function loadShields() {
))
);
+ // Arizona
+ shields["US:AZ:Scenic"] = {
+ spriteBlank: "shield_us_az_scenic",
+ notext: true,
+ };
+
// Arkansas
shields["US:AR"] = {
spriteBlank: ["shield_us_ar_2", "shield_us_ar_3"],
@@ -766,11 +775,14 @@ export function loadShields() {
["BUS"],
Color.shields.green
);
- shields["US:CA:CR"] = pentagonUpShield(
- 3,
- 15,
- Color.shields.blue,
- Color.shields.yellow
+ ["CR", "Sierra"].forEach(
+ (county) =>
+ (shields[`US:CA:${county}`] = pentagonUpShield(
+ 3,
+ 15,
+ Color.shields.blue,
+ Color.shields.yellow
+ ))
);
shields["US:CA:Mendocino"] = roundedRectShield(
Color.shields.green,
@@ -841,6 +853,10 @@ export function loadShields() {
Color.shields.green,
Color.shields.white
);
+ shields["US:CO:Weld:WCP"] = {
+ ...pentagonUpShield(3, 15, Color.shields.blue, Color.shields.green),
+ textHaloColor: Color.shields.white,
+ };
// Connecticut
shields["US:CT"] = roundedRectShield(
@@ -1128,8 +1144,8 @@ export function loadShields() {
"Webster",
"Winn",
].forEach(
- (county) =>
- (shields[`US:LA:${county}`] = pentagonUpShield(
+ (parish) =>
+ (shields[`US:LA:${parish}`] = pentagonUpShield(
3,
15,
Color.shields.blue,
@@ -1266,7 +1282,6 @@ export function loadShields() {
"Lake",
"Lake_of_the_Woods",
"Le_Sueur",
- "Lincoln",
"Lyon",
"Mahnomen",
"Marshall",
@@ -1292,7 +1307,6 @@ export function loadShields() {
"Redwood",
"Renville",
"Rice",
- "Rock",
"Roseau",
"Saint_Louis",
"Scott",
@@ -1315,11 +1329,7 @@ export function loadShields() {
"Yellow_Medicine",
].forEach(
(county) =>
- ([
- shields[`US:MN:${county}:CSAH`],
- shields[`US:MN:${county}:CR`],
- shields[`US:MN:${county}:Park_Access`],
- ] = [
+ ([shields[`US:MN:${county}:CSAH`], shields[`US:MN:${county}:CR`]] = [
pentagonUpShield(
3,
15,
@@ -1328,15 +1338,32 @@ export function loadShields() {
Color.shields.white
),
roundedRectShield(Color.shields.white, Color.shields.black),
- trapezoidDownShield(
- 10,
- Color.shields.brown,
- Color.shields.white,
- Color.shields.white,
- 2
- ),
])
);
+ ["CSAH", "CR"].forEach(
+ (network) =>
+ (shields[`US:MN:Lincoln:${network}`] = pentagonUpShield(
+ 3,
+ 15,
+ Color.shields.blue,
+ Color.shields.yellow,
+ Color.shields.white
+ ))
+ );
+ ["CSAH", "CR"].forEach(
+ (network) =>
+ (shields[`US:MN:Rock:${network}`] = roundedRectShield(
+ Color.shields.white,
+ Color.shields.black
+ ))
+ );
+ shields[`US:MN:Hennepin:Park_Access`] = trapezoidDownShield(
+ 10,
+ Color.shields.brown,
+ Color.shields.white,
+ Color.shields.white,
+ 2
+ );
// Missouri
shields["US:MO"] = {
@@ -1400,6 +1427,11 @@ export function loadShields() {
// Mississippi
shields["US:MS"] = ovalShield(Color.shields.white, Color.shields.black);
+ shields["US:MS:Scenic"] = banneredShield(
+ ovalShield(Color.shields.white, Color.shields.blue),
+ ["SCEN"],
+ Color.shields.blue
+ );
[
"Alcorn",
"Calhoun",
@@ -1632,6 +1664,13 @@ export function loadShields() {
["TRK"],
Color.shields.blue
);
+ ["Sussex", "Warren"].forEach(
+ (county) =>
+ (shields[`US:NJ:${county}:NPS`] = pillShield(
+ Color.shields.brown,
+ Color.shields.white
+ ))
+ );
// New Mexico
shields["US:NM"] = pillShield(
@@ -2236,6 +2275,10 @@ export function loadShields() {
bottom: 8,
},
};
+ shields["US:SD:Custer:NPS"] = roundedRectShield(
+ Color.shields.brown,
+ Color.shields.yellow
+ );
// Tennessee
shields["US:TN:primary"] = {
@@ -2449,11 +2492,14 @@ export function loadShields() {
bottom: 5,
},
};
- shields["US:UT:Wayne"] = pentagonUpShield(
- 3,
- 15,
- Color.shields.blue,
- Color.shields.yellow
+ ["Wayne", "Washington"].forEach(
+ (county) =>
+ (shields[`US:UT:${county}`] = pentagonUpShield(
+ 3,
+ 15,
+ Color.shields.blue,
+ Color.shields.yellow
+ ))
);
// Virginia
@@ -3320,6 +3366,7 @@ export function loadShields() {
shields["omt-ie-national"] = roundedRectShield(
Color.shields.green,
+ Color.shields.white,
Color.shields.yellow
);
@@ -3535,6 +3582,7 @@ export function loadShields() {
shields["omt-gb-trunk"] = roundedRectShield(
Color.shields.green,
+ Color.shields.white,
Color.shields.yellow
);
@@ -3616,6 +3664,45 @@ export function loadShields() {
// Ref-specific cases. Each entry should be documented in CONTRIBUTE.md
+ shields["CA:NS:S"].overrideByName = {
+ "Bras d'Or Lakes Scenic Drive": {
+ spriteBlank: "shield_ca_ns_s_bdolsd",
+ },
+ "Ceilidh Trail": {
+ spriteBlank: "shield_ca_ns_s_cet",
+ },
+ "Cabot Trail": {
+ spriteBlank: "shield_ca_ns_s_ct",
+ },
+ "Digby Neck and Islands Scenic Drive": {
+ spriteBlank: "shield_ca_ns_s_dnisd",
+ },
+ "Evangeline Trail": {
+ spriteBlank: "shield_ca_ns_s_et",
+ },
+ "Fleur-de-lis Trail": {
+ spriteBlank: "shield_ca_ns_s_fdlt",
+ },
+ "Glooscap Trail": {
+ spriteBlank: "shield_ca_ns_s_gt",
+ },
+ "Kejimkujik Scenic Drive": {
+ spriteBlank: "shield_ca_ns_s_ksd",
+ },
+ "Lighthouse Route": {
+ spriteBlank: "shield_ca_ns_s_lr",
+ },
+ "Marine Drive": {
+ spriteBlank: "shield_ca_ns_s_md",
+ },
+ "Marconi Trail": {
+ spriteBlank: "shield_ca_ns_s_mt",
+ },
+ "Sunrise Trail": {
+ spriteBlank: "shield_ca_ns_s_st",
+ },
+ };
+
shields["CA:ON:primary"].overrideByRef = {
QEW: {
textColor: Color.shields.blue,
@@ -3652,23 +3739,13 @@ export function loadShields() {
};
shields["US:KY:Parkway"].refsByName = {
- // FIXME: This object contains both spelled-out and abbreviated road
- // names to accommodate both the abbreviated names from OpenMapTiles and
- // the spelled-out names from Planetiler.
- // https://github.com/onthegomap/planetiler/issues/14
"Audubon Parkway": "AU",
"Bluegrass Parkway": "BG",
- "Bluegrass Pkwy": "BG",
"Cumberland Parkway": "LN",
- "Cumberland Pkwy": "LN",
"Hal Rogers Parkway": "HR",
- "Hal Rogers Pkwy": "HR",
"Mountain Parkway": "MP",
- "Mountain Pkwy": "MP",
"Purchase Parkway": "JC",
- "Purchase Pkwy": "JC",
"Western Kentucky Parkway": "WK",
- "Western Kentucky Pkwy": "WK",
};
shields["US:CT:Parkway"].overrideByName = {
@@ -3708,15 +3785,13 @@ export function loadShields() {
};
shields["US:NY:Parkway"].refsByName = {
- "Bear Mountain Parkway": "BMP",
- "Bronx and Pelham Parkway": "PP",
+ "Bear Mountain State Parkway": "BMP",
"Bronx River Parkway": "BRP",
"Cross County Parkway": "CCP",
"Hutchinson River Parkway": "HRP",
"Korean War Veterans Parkway": "KWVP",
"Mosholu Parkway": "MP",
"Niagara Scenic Parkway": "NSP",
- "Pelham Parkway": "PP",
"Saw Mill River Parkway": "SMP",
"Sprain Brook Parkway": "SBP",
"Taconic State Parkway": "TSP",
@@ -3736,14 +3811,7 @@ export function loadShields() {
"Fort Bend Westpark Tollway": "WPT",
};
shields["US:TX:Harris:HCTRA"].refsByName = {
- "East Sam Houston Tollway North": "SHT",
- "East Sam Houston Tollway South": "SHT",
- "North Sam Houston Tollway East": "SHT",
- "North Sam Houston Tollway West": "SHT",
- "South Sam Houston Tollway East": "SHT",
- "South Sam Houston Tollway West": "SHT",
- "West Sam Houston Tollway North": "SHT",
- "West Sam Houston Tollway South": "SHT",
+ "Sam Houston Tollway": "SHT",
"Fort Bend Toll Road": "FBTR",
"Hardy Toll Road": "HTR",
"Tomball Tollway": "TBT",
diff --git a/src/js/shield_format.ts b/src/js/shield_format.ts
index ae6ffdd32..10368bbf5 100644
--- a/src/js/shield_format.ts
+++ b/src/js/shield_format.ts
@@ -3,6 +3,8 @@ import type {
RouteParser,
} from "@americana/maplibre-shield-generator";
+import { parseImageName } from "../layer/highway_shield.js";
+
export const shieldPredicate: StringPredicate = (imageID: string) =>
imageID && imageID.startsWith("shield");
@@ -14,16 +16,8 @@ export const networkPredicate: StringPredicate = (network: string) =>
export const routeParser: RouteParser = {
parse: (id: string) => {
- //Americana format is `${shield}\n${network}=${ref}\n${name}`
- let id_parts: string[] = id.split("\n");
- let network_ref = id_parts[1].split("=");
-
- return {
- network: network_ref[0],
- ref: network_ref[1],
- name: id_parts[2],
- };
+ return parseImageName(id);
},
format: (network: string, ref: string, name: string) =>
- `shield\n${network}=${ref}\n${name}`,
+ `shield\n${network}\n${ref}\n${name}\n`,
};
diff --git a/src/layer/highway_shield.js b/src/layer/highway_shield.js
index 319a37224..6c24abb43 100644
--- a/src/layer/highway_shield.js
+++ b/src/layer/highway_shield.js
@@ -1,33 +1,23 @@
"use strict";
-export const namedRouteNetworks = [
- "US:CT:Parkway",
- "US:KY:Parkway",
- "US:NH:Turnpike",
- "US:NY:Parkway",
- "US:TX:Fort_Bend:FBCTRA",
- "US:TX:Harris:HCTRA",
-];
+const orderedRouteAttributes = ["network", "ref", "name", "color"];
export function getImageNameExpression(routeIndex) {
- return [
- "concat",
- "shield\n",
- ["get", "route_" + routeIndex],
- [
- "match",
- ["get", "route_" + routeIndex],
- namedRouteNetworks.map((n) => n + "="),
- ["concat", "\n", ["get", "name"]],
- "",
- ],
- ];
+ let concat = ["concat", "shield"];
+ for (let attr of orderedRouteAttributes) {
+ concat.push("\n");
+ concat.push(["coalesce", ["get", `route_${routeIndex}_${attr}`], ""]);
+ }
+ return concat;
}
function routeConcurrency(routeIndex) {
return [
"case",
- ["!=", ["get", "route_" + routeIndex], null],
+ [
+ "any",
+ ...orderedRouteAttributes.map((a) => ["has", `route_${routeIndex}_${a}`]),
+ ],
["image", getImageNameExpression(routeIndex)],
["literal", ""],
];
@@ -37,12 +27,16 @@ function routeConcurrency(routeIndex) {
* Returns a structured representation of the given image name.
*
* @param name An image name in the format returned by `routeConcurrency`.
+ * @return An object with the keys in `orderedRouteAttributes` plus the full image name in `imageName`.
*/
export function parseImageName(imageName) {
let lines = imageName.split("\n");
- let [, network, ref] = lines[1].match(/^(.*?)=(.*)/) || [];
- let name = lines[2];
- return { imageName, network, ref, name };
+ lines.shift(); // "shield"
+ let parsed = Object.fromEntries(
+ orderedRouteAttributes.map((a, i) => [a, lines[i]])
+ );
+ parsed.imageName = imageName;
+ return parsed;
}
let shieldTextField = ["format"];
@@ -113,11 +107,6 @@ export const shield = {
},
filter: [
"any",
- ["has", "route_1"],
- ["has", "route_2"],
- ["has", "route_3"],
- ["has", "route_4"],
- ["has", "route_5"],
- ["has", "route_6"],
+ ...orderedRouteAttributes.map((a) => ["has", `route_1_${a}`]),
],
};
diff --git a/src/layer/poi.js b/src/layer/poi.js
index c6a9ff369..64279b01d 100644
--- a/src/layer/poi.js
+++ b/src/layer/poi.js
@@ -19,6 +19,14 @@ var iconDefs = {
color: Color.poi.consumer,
description: "Bar or pub",
},
+ bookstore: {
+ classes: {
+ library: ["books"],
+ },
+ sprite: "poi_book_upright",
+ color: Color.poi.consumer,
+ description: "Bookstore",
+ },
bus_station: {
classes: {
bus: ["bus_station"],
@@ -35,6 +43,22 @@ var iconDefs = {
color: Color.poi.transport,
description: "Bus stop",
},
+ car_repair: {
+ classes: {
+ car: ["car_repair"],
+ },
+ sprite: "poi_car_repair",
+ color: Color.poi.consumer,
+ description: "Car mechanic",
+ },
+ car_shop: {
+ classes: {
+ car: ["car"],
+ },
+ sprite: "poi_car_shop",
+ color: Color.poi.consumer,
+ description: "Car dealership",
+ },
cemetery: {
classes: {
cemetery: ["cemetery"],
@@ -42,6 +66,14 @@ var iconDefs = {
sprite: "poi_gravestone",
color: Color.poi.outdoor,
decription: "Cemetery",
+ },
+ taxi: {
+ classes: {
+ office: ["taxi"],
+ },
+ sprite: "poi_taxi",
+ color: Color.poi.transport,
+ description: "Taxi stand",
},
coffee: {
classes: {
@@ -67,6 +99,30 @@ var iconDefs = {
color: Color.poi.infrastructure,
description: "Hospital",
},
+ hotel: {
+ classes: {
+ lodging: ["hotel", "motel", "guest_house"],
+ },
+ sprite: "poi_hotel",
+ color: Color.poi.consumer,
+ description: "Hotel",
+ },
+ hostel: {
+ classes: {
+ lodging: ["hostel"],
+ },
+ sprite: "poi_hostel",
+ color: Color.poi.consumer,
+ description: "Hostel",
+ },
+ library: {
+ classes: {
+ library: ["library"],
+ },
+ sprite: "poi_book_upright",
+ color: Color.poi.infrastructure,
+ description: "Library",
+ },
medical: {
classes: {
hospital: ["clinic"],
@@ -100,6 +156,14 @@ var iconDefs = {
color: Color.poi.infrastructure,
description: "Police station",
},
+ post_office: {
+ classes: {
+ post: ["post_office"],
+ },
+ sprite: "poi_envelope",
+ color: Color.poi.infrastructure,
+ description: "Post office",
+ },
pow_buddhist: {
classes: {
place_of_worship: ["buddhist"],
@@ -265,8 +329,13 @@ export const poi = {
[
...getSubclasses(iconDefs.fuel),
...getSubclasses(iconDefs.bar),
+ ...getSubclasses(iconDefs.bookstore),
...getSubclasses(iconDefs.coffee),
...getSubclasses(iconDefs.supermarket),
+ ...getSubclasses(iconDefs.car_shop),
+ ...getSubclasses(iconDefs.car_repair),
+ ...getSubclasses(iconDefs.hotel),
+ ...getSubclasses(iconDefs.hostel),
],
Color.poi.consumer,
[
@@ -274,6 +343,7 @@ export const poi = {
"bus_stop",
...getSubclasses(iconDefs.railway_station),
...getSubclasses(iconDefs.railway_stop),
+ ...getSubclasses(iconDefs.taxi),
],
Color.poi.transport,
["museum"],
@@ -283,8 +353,10 @@ export const poi = {
"parking",
"police",
"school",
- "college",
+ ...getSubclasses(iconDefs.college),
+ "library",
"townhall",
+ ...getSubclasses(iconDefs.post_office),
...getSubclasses(iconDefs.pow_christian),
...getSubclasses(iconDefs.pow_buddhist),
...getSubclasses(iconDefs.pow_hindu),
@@ -306,16 +378,24 @@ export const poi = {
[
"match",
["get", "subclass"],
- ["station", "halt"],
+ [
+ ...getSubclasses(iconDefs.college),
+ ],
+ 10,
+ [
+ "station",
+ "halt"],
12,
- ["bus_station", "subway", ...getSubclasses(iconDefs.college)],
+ ["bus_station", "subway"],
14,
[
"bus_stop",
"hospital",
+ "library",
"museum",
"police",
...getSubclasses(iconDefs.fuel),
+ ...getSubclasses(iconDefs.post_office),
...getSubclasses(iconDefs.pow_buddhist),
...getSubclasses(iconDefs.pow_christian),
...getSubclasses(iconDefs.pow_hindu),
@@ -332,8 +412,14 @@ export const poi = {
15,
[
...getSubclasses(iconDefs.bar),
- ...getSubclasses(iconDefs.coffee),
+ ...getSubclasses(iconDefs.bookstore),
...getSubclasses(iconDefs.cemetery),
+ ...getSubclasses(iconDefs.coffee),
+ ...getSubclasses(iconDefs.car_shop),
+ ...getSubclasses(iconDefs.car_repair),
+ ...getSubclasses(iconDefs.taxi),
+ ...getSubclasses(iconDefs.hotel),
+ ...getSubclasses(iconDefs.hostel),
],
16,
["clinic", "doctors", "parking"],
diff --git a/test/sample_locations.json b/test/sample_locations.json
index 2ff11454f..dd5ba8dfe 100644
--- a/test/sample_locations.json
+++ b/test/sample_locations.json
@@ -48,6 +48,14 @@
"height": 400
}
},
+ {
+ "location": "17/40.753922/-73.984244",
+ "name": "library_z17",
+ "viewport": {
+ "width": 400,
+ "height": 400
+ }
+ },
{
"location": "13.25/49.552/5.8107",
"name": "europe_e-road_routes",
@@ -55,5 +63,29 @@
"width": 400,
"height": 400
}
+ },
+ {
+ "location": "16/38.896954/-77.108406",
+ "name": "virginia_",
+ "viewport": {
+ "width": 400,
+ "height": 400
+ }
+ },
+ {
+ "location": "16/38.906341/-77.025889",
+ "name": "dc_logan_circle",
+ "viewport": {
+ "width": 400,
+ "height": 400
+ }
+ },
+ {
+ "location": "16/40.755264/-73.987405",
+ "name": "manhattan_times_square",
+ "viewport": {
+ "width": 400,
+ "height": 400
+ }
}
]
diff --git a/test/shield_format/shield_format.spec.ts b/test/shield_format/shield_format.spec.ts
index 0c72201ef..f79177a7e 100644
--- a/test/shield_format/shield_format.spec.ts
+++ b/test/shield_format/shield_format.spec.ts
@@ -1,10 +1,11 @@
import { expect } from "chai";
import { shieldPredicate, routeParser } from "../../src/js/shield_format.js";
-const image_id_I95 = "shield\nUS:I=95";
+const image_id_I95 = "shield\nUS:I\n95\nEye Ninety-Five";
const route_def_I95 = {
network: "US:I",
ref: "95",
+ name: "Eye Ninety-Five",
};
describe("shield_format", function () {
@@ -19,6 +20,7 @@ describe("shield_format", function () {
let extractedDef = routeParser.parse(image_id_I95);
expect(extractedDef.network).to.be.equal(route_def_I95.network);
expect(extractedDef.ref).to.be.equal(route_def_I95.ref);
+ expect(extractedDef.name).to.be.equal(route_def_I95.name);
});
});
});
diff --git a/test/spec/highway_shield.js b/test/spec/highway_shield.js
index 14df94866..20939d3f4 100644
--- a/test/spec/highway_shield.js
+++ b/test/spec/highway_shield.js
@@ -17,20 +17,20 @@ describe("highway_shield", function () {
.createExpression(HighwayShieldLayers.getImageNameExpression(1))
.value.expression.evaluate(expressionContext(properties));
- let expectImageName = (network, ref, name, expectedImageName) => {
+ let expectImageName = (network, ref, name, color, expectedImageName) => {
let properties = {
- route_1: `${network || ""}=${ref || ""}`,
- name: name || null,
+ route_1_network: network || "",
+ route_1_ref: ref || "",
+ route_1_name: name || "",
+ route_1_color: color || "",
};
let evaluated = evaluatedExpression(properties);
let expectedProperties = {
imageName: expectedImageName,
network: network || "",
ref: ref || "",
- name:
- !ref && HighwayShieldLayers.namedRouteNetworks.includes(network)
- ? name
- : undefined,
+ name: name || "",
+ color: color || "",
};
expect(HighwayShieldLayers.parseImageName(evaluated)).to.be.deep.equal(
expectedProperties
@@ -38,28 +38,54 @@ describe("highway_shield", function () {
};
it("parses an image name for a numbered route", function () {
- expectImageName("NET", "REF", undefined, "shield\nNET=REF");
- expectImageName("NET", "REF", "NAME", "shield\nNET=REF");
+ expectImageName(
+ "NET",
+ "REF",
+ undefined,
+ undefined,
+ "shield\nNET\nREF\n\n"
+ );
+ expectImageName(
+ "NET",
+ "REF",
+ "NAME",
+ undefined,
+ "shield\nNET\nREF\nNAME\n"
+ );
});
it("parses an image name for an unnumbered route", function () {
- expectImageName("NET", undefined, undefined, "shield\nNET=");
+ expectImageName(
+ "NET",
+ undefined,
+ undefined,
+ undefined,
+ "shield\nNET\n\n\n"
+ );
});
it("parses an image name for a named route", function () {
expectImageName(
"US:KY:Parkway",
undefined,
"NAME",
- "shield\nUS:KY:Parkway=\nNAME"
+ undefined,
+ "shield\nUS:KY:Parkway\n\nNAME\n"
);
expectImageName(
"US:KY:Parkway",
"REF",
"NAME",
- "shield\nUS:KY:Parkway=REF"
+ undefined,
+ "shield\nUS:KY:Parkway\nREF\nNAME\n"
);
});
it("parses an image name for a network-independent route", function () {
- expectImageName(undefined, "REF", "NAME", "shield\n=REF");
+ expectImageName(
+ undefined,
+ "REF",
+ "NAME",
+ undefined,
+ "shield\n\nREF\nNAME\n"
+ );
});
});
});
diff --git a/test/spec/shield.js b/test/spec/shield.js
index 2fb4b4bf2..29beb569d 100644
--- a/test/spec/shield.js
+++ b/test/spec/shield.js
@@ -25,11 +25,11 @@ const shieldRenderer = new ShieldRenderer(shields, routeParser)
const handler = shieldRenderer.getStyleImageMissingHandler();
-handler({ id: "shield\nBAB=5" });
-handler({ id: "shield\nUS:RI=" });
-handler({ id: "shield\nUS:RI=ABC123" });
-handler({ id: "shield\nUS:RI=Equator" });
-handler({ id: "shield\nrwn=" });
+handler({ id: "shield\nBAB\n5\n\n" });
+handler({ id: "shield\nUS:RI\n\n\n" });
+handler({ id: "shield\nUS:RI\nABC123\n\n" });
+handler({ id: "shield\nUS:RI\nEquator\n\n" });
+handler({ id: "shield\nrwn\n\n\n" });
handler({ id: "foo" });
function isBlankSprite(id) {
@@ -39,8 +39,8 @@ function isBlankSprite(id) {
describe("shield", function () {
describe("#isValidNetwork", function () {
it("rejects a recreational network", function () {
- expect(isBlankSprite("shield\nBAB=5")).to.be.false;
- expect(isBlankSprite("shield\nrwn=")).to.be.true;
+ expect(isBlankSprite("shield\nBAB\n5\n\n")).to.be.false;
+ expect(isBlankSprite("shield\nrwn\n\n\n")).to.be.true;
});
it("rejects other missing image prefixes", function () {
expect(mockRepo.hasSprite("foo")).to.be.false;
@@ -48,12 +48,12 @@ describe("shield", function () {
});
describe("#isValidRef", function () {
it("rejects an empty ref", function () {
- expect(isBlankSprite("shield\nUS:RI=")).to.be.true;
+ expect(isBlankSprite("shield\nUS:RI\n\n\n")).to.be.true;
});
it("rejects a long ref", function () {
- expect(mockRepo.hasSprite("shield\nUS:RI=ABC123")).to.be.true;
- expect(isBlankSprite("shield\nUS:RI=ABC123")).to.be.false;
- expect(isBlankSprite("shield\nUS:RI=Equator")).to.be.true;
+ expect(mockRepo.hasSprite("shield\nUS:RI\nABC123\n\n")).to.be.true;
+ expect(isBlankSprite("shield\nUS:RI\nABC123\n\n")).to.be.false;
+ expect(isBlankSprite("shield\nUS:RI\nEquator\n\n")).to.be.true;
});
});
});