From 7ae1aaf9787e20ca4671264cc43627930d5cf17b Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 30 May 2024 09:14:08 +0100 Subject: [PATCH 1/5] feat(frontend): Vector base maps - Migrate from `mapbox-gl` to `maplibre-gl`. - Replace `DeckMap` with `BaseMap`, `and `DeckGLOverlay` for data layers. - Refactor map controls and map view state. - Roll back concurrent rendering, which seems to be causing a null ref error inside React Map GL when it instantiates `mapbox`. --- frontend/package-lock.json | 276 +- frontend/package.json | 9 +- frontend/public/map-styles/map-style.json | 3011 +++++++++++++++++ frontend/src/App.tsx | 2 +- frontend/src/config/basemaps.ts | 128 + frontend/src/config/map-view.ts | 2 +- frontend/src/index.tsx | 5 +- frontend/src/lib/data-map/BaseMap.tsx | 34 + frontend/src/lib/data-map/DataMap.tsx | 108 +- frontend/src/lib/data-map/DeckMap.tsx | 99 - .../data-map/MapContextProviderWithLimits.tsx | 38 - frontend/src/lib/data-map/ViewStateDebug.tsx | 18 - .../src/lib/data-map/use-data-load-trigger.ts | 46 + .../src/lib/deck/layers/data-loader-layer.ts | 4 +- frontend/src/lib/deck/props/data-source.ts | 4 +- .../src/lib/hooks/use-throttled-callback.ts | 33 + frontend/src/lib/map/DeckGLOverlay.tsx | 14 + frontend/src/lib/map/MapBoundsFitter.tsx | 38 +- .../{mapbox-controls.tsx => map-controls.tsx} | 0 .../src/lib/recoil/sync-state-throttled.ts | 19 + frontend/src/map/MapView.tsx | 60 +- frontend/src/map/layers/layers-state.ts | 2 +- frontend/src/map/use-background-config.ts | 23 - frontend/src/map/use-basemap-style.ts | 73 + frontend/src/state/layers/view-layers.ts | 24 +- frontend/src/state/map-view/map-url.ts | 69 + frontend/src/state/map-view/map-view-state.ts | 58 + 27 files changed, 3768 insertions(+), 429 deletions(-) create mode 100644 frontend/public/map-styles/map-style.json create mode 100644 frontend/src/config/basemaps.ts create mode 100644 frontend/src/lib/data-map/BaseMap.tsx delete mode 100644 frontend/src/lib/data-map/DeckMap.tsx delete mode 100644 frontend/src/lib/data-map/MapContextProviderWithLimits.tsx delete mode 100644 frontend/src/lib/data-map/ViewStateDebug.tsx create mode 100644 frontend/src/lib/data-map/use-data-load-trigger.ts create mode 100644 frontend/src/lib/hooks/use-throttled-callback.ts create mode 100644 frontend/src/lib/map/DeckGLOverlay.tsx rename frontend/src/lib/map/hud/{mapbox-controls.tsx => map-controls.tsx} (100%) create mode 100644 frontend/src/lib/recoil/sync-state-throttled.ts delete mode 100644 frontend/src/map/use-background-config.ts create mode 100644 frontend/src/map/use-basemap-style.ts create mode 100644 frontend/src/state/map-view/map-url.ts create mode 100644 frontend/src/state/map-view/map-view-state.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index d5bfb197..3d009533 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -30,10 +30,10 @@ "immer": "^10.1.1", "json-stable-stringify": "^1.0.1", "lodash": "^4.17.21", - "mapbox-gl": "^1.13.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-map-gl": "^5.3.21", + "maplibre-gl": "^4.3.2", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-map-gl": "^7.1.0", "react-router-dom": "^6.23.1", "react-spring-bottom-sheet": "^3.4.1", "react-vega": "^7.6.0", @@ -55,7 +55,6 @@ "@types/geojson": "^7946.0.8", "@types/json-stable-stringify": "^1.0.34", "@types/lodash": "^4.14.173", - "@types/mapbox-gl": "^1.13.2", "@types/node": "^20.12.12", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", @@ -3198,11 +3197,6 @@ "geojson-rewind": "geojson-rewind" } }, - "node_modules/@mapbox/geojson-types": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", - "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==" - }, "node_modules/@mapbox/glyph-pbf-composite": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/@mapbox/glyph-pbf-composite/-/glyph-pbf-composite-0.0.3.tgz", @@ -3220,12 +3214,11 @@ } }, "node_modules/@mapbox/mapbox-gl-supported": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", - "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", - "peerDependencies": { - "mapbox-gl": ">=0.32.1 <2.0.0" - } + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-2.0.1.tgz", + "integrity": "sha512-HP6XvfNIzfoMVfyGjBckjiAOQK9WfX0ywdLubuPMPv+Vqf5fj0uCbgBQYpiqcWZT6cbyyRnTSXDheT1ugvF6UQ==", + "optional": true, + "peer": true }, "node_modules/@mapbox/martini": { "version": "0.2.0", @@ -3306,9 +3299,9 @@ "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" }, "node_modules/@mapbox/unitbezier": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", - "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", + "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" }, "node_modules/@mapbox/vector-tile": { "version": "1.3.1", @@ -3346,11 +3339,6 @@ "gl-style-validate": "dist/gl-style-validate.mjs" } }, - "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" - }, "node_modules/@math.gl/core": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/@math.gl/core/-/core-3.6.3.tgz", @@ -4646,6 +4634,14 @@ "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz", "integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg==" }, + "node_modules/@types/geojson-vt": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", + "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/hammerjs": { "version": "2.0.45", "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.45.tgz", @@ -4669,6 +4665,11 @@ "integrity": "sha512-b7bq23s4fgBB76n34m2b3RBf6M369B0Z9uRR8aHTMd8kZISRkmDEpPD8hhpYvDFzr3bJCPES96cm3Q6qRNDbQw==", "dev": true }, + "node_modules/@types/junit-report-builder": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/junit-report-builder/-/junit-report-builder-3.0.2.tgz", + "integrity": "sha512-R5M+SYhMbwBeQcNXYWNCZkl09vkVfAtcPIaCGdzIkkbeaTrVbGQ7HVgi4s+EmM/M1K4ZuWQH0jGcvMvNePfxYA==" + }, "node_modules/@types/leaflet": { "version": "1.9.12", "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz", @@ -4683,13 +4684,19 @@ "integrity": "sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==", "dev": true }, - "node_modules/@types/mapbox-gl": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-1.13.10.tgz", - "integrity": "sha512-0oUy5d5nT3L480MRviAnaBUEXuWCG/7M4ZQo0n8eJ/LLMgJ0nMbjv7M+qoPl4TAj6yVVWKTvkukXvW9QHH1GVw==", - "dev": true, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", + "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", + "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", "dependencies": { - "@types/geojson": "*" + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" } }, "node_modules/@types/minimist": { @@ -4721,6 +4728,11 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==" }, + "node_modules/@types/pbf": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", + "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==" + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -4773,6 +4785,14 @@ "@types/react": "*" } }, + "node_modules/@types/supercluster": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", + "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", @@ -6177,7 +6197,9 @@ "node_modules/csscolorparser": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", - "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==" + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "optional": true, + "peer": true }, "node_modules/csso": { "version": "5.0.5", @@ -8024,6 +8046,30 @@ "node": ">=10.13.0" } }, + "node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -8101,7 +8147,9 @@ "node_modules/grid-index": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", - "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==" + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "optional": true, + "peer": true }, "node_modules/h3-js": { "version": "3.7.2", @@ -8928,8 +8976,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isobject": { "version": "3.0.1", @@ -9089,9 +9136,9 @@ } }, "node_modules/kdbush": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", - "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==" + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" }, "node_modules/keyv": { "version": "4.5.4", @@ -9279,42 +9326,77 @@ } }, "node_modules/mapbox-gl": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", - "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-2.15.0.tgz", + "integrity": "sha512-fjv+aYrd5TIHiL7wRa+W7KjtUqKWziJMZUkK5hm8TvJ3OLeNPx4NmW/DgfYhd/jHej8wWL+QJBDbdMMAKvNC0A==", + "optional": true, + "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/geojson-types": "^1.0.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/mapbox-gl-supported": "^2.0.1", "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^1.1.1", - "@mapbox/unitbezier": "^0.0.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", "@mapbox/vector-tile": "^1.3.1", "@mapbox/whoots-js": "^3.1.0", "csscolorparser": "~1.0.3", - "earcut": "^2.2.2", + "earcut": "^2.2.4", "geojson-vt": "^3.2.1", - "gl-matrix": "^3.2.1", + "gl-matrix": "^3.4.3", "grid-index": "^1.1.0", + "kdbush": "^4.0.1", "murmurhash-js": "^1.0.0", "pbf": "^3.2.1", - "potpack": "^1.0.1", + "potpack": "^2.0.0", "quickselect": "^2.0.0", "rw": "^1.3.3", - "supercluster": "^7.1.0", + "supercluster": "^8.0.0", "tinyqueue": "^2.0.3", - "vt-pbf": "^3.1.1" + "vt-pbf": "^3.1.3" + } + }, + "node_modules/maplibre-gl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.3.2.tgz", + "integrity": "sha512-/oXDsb9I+LkjweL/28aFMLDZoIcXKNEhYNAZDLA4xgTNkfvKQmV/r0KZdxEMcVthincJzdyc6Y4N8YwZtHKNnQ==", + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^2.0.6", + "@mapbox/unitbezier": "^0.0.1", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "@maplibre/maplibre-gl-style-spec": "^20.2.0", + "@types/geojson": "^7946.0.14", + "@types/geojson-vt": "3.2.5", + "@types/junit-report-builder": "^3.0.2", + "@types/mapbox__point-geometry": "^0.1.4", + "@types/mapbox__vector-tile": "^1.3.4", + "@types/pbf": "^3.0.5", + "@types/supercluster": "^7.1.3", + "earcut": "^2.2.4", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.4.3", + "global-prefix": "^3.0.0", + "kdbush": "^4.0.2", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^2.0.0", + "quickselect": "^2.0.0", + "supercluster": "^8.0.1", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.3" }, "engines": { - "node": ">=6.4.0" + "node": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" } }, - "node_modules/mapbox-gl/node_modules/@mapbox/tiny-sdf": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", - "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==" - }, "node_modules/math.gl": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/math.gl/-/math.gl-3.6.3.tgz", @@ -10284,9 +10366,9 @@ } }, "node_modules/potpack": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", - "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", + "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==" }, "node_modules/prebuild-install": { "version": "7.1.2", @@ -10577,25 +10659,44 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react-map-gl": { - "version": "5.3.21", - "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-5.3.21.tgz", - "integrity": "sha512-hNVYiPBjgfVIcDV70OU9QnzvNCI1NhLm5OHjyY1rKPOKqzV4m9jjuXEKUaWC72vqIHk1Dzb+gG78xWOpqVi6uw==", + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/react-map-gl/-/react-map-gl-7.1.7.tgz", + "integrity": "sha512-mwjc0obkBJOXCcoXQr3VoLqmqwo9vS4bXfbGsdxXzEgVCv/PM0v+1QggL7W0d/ccIy+VCjbXNlGij+PENz6VNg==", "dependencies": { - "@babel/runtime": "^7.0.0", - "@types/geojson": "^7946.0.7", - "@types/mapbox-gl": "^2.0.3", - "mapbox-gl": "^1.0.0", - "mjolnir.js": "^2.5.0", - "prop-types": "^15.7.2", - "resize-observer-polyfill": "^1.5.1", - "viewport-mercator-project": "^7.0.4" - }, - "engines": { - "node": ">= 4", - "npm": ">= 3" + "@maplibre/maplibre-gl-style-spec": "^19.2.1", + "@types/mapbox-gl": ">=1.0.0" }, "peerDependencies": { - "react": ">=16.3.0" + "mapbox-gl": ">=1.13.0", + "maplibre-gl": ">=1.13.0", + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + }, + "peerDependenciesMeta": { + "mapbox-gl": { + "optional": true + }, + "maplibre-gl": { + "optional": true + } + } + }, + "node_modules/react-map-gl/node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "19.3.3", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.3.tgz", + "integrity": "sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==", + "dependencies": { + "@mapbox/jsonlint-lines-primitives": "~2.0.2", + "@mapbox/unitbezier": "^0.0.1", + "json-stringify-pretty-compact": "^3.0.0", + "minimist": "^1.2.8", + "rw": "^1.3.3", + "sort-object": "^3.0.3" + }, + "bin": { + "gl-style-format": "dist/gl-style-format.mjs", + "gl-style-migrate": "dist/gl-style-migrate.mjs", + "gl-style-validate": "dist/gl-style-validate.mjs" } }, "node_modules/react-map-gl/node_modules/@types/mapbox-gl": { @@ -10606,6 +10707,11 @@ "@types/geojson": "*" } }, + "node_modules/react-map-gl/node_modules/json-stringify-pretty-compact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-3.0.0.tgz", + "integrity": "sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==" + }, "node_modules/react-refresh": { "version": "0.14.2", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", @@ -11091,11 +11197,6 @@ "node": ">=0.10.0" } }, - "node_modules/resize-observer-polyfill": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" - }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -11908,11 +12009,11 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" }, "node_modules/supercluster": { - "version": "7.1.5", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", - "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", "dependencies": { - "kdbush": "^3.0.0" + "kdbush": "^4.0.2" } }, "node_modules/supports-color": { @@ -13196,15 +13297,6 @@ "vega-util": "^1.17.1" } }, - "node_modules/viewport-mercator-project": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/viewport-mercator-project/-/viewport-mercator-project-7.0.4.tgz", - "integrity": "sha512-0jzpL6pIMocCKWg1C3mqi/N4UPgZC3FzwghEm1H+XsUo8hNZAyJc3QR7YqC816ibOR8aWT5pCsV+gCu8/BMJgg==", - "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", - "dependencies": { - "@math.gl/web-mercator": "^3.5.5" - } - }, "node_modules/vite": { "version": "5.2.12", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz", diff --git a/frontend/package.json b/frontend/package.json index 3edc0d18..83816c0b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,10 +26,10 @@ "immer": "^10.1.1", "json-stable-stringify": "^1.0.1", "lodash": "^4.17.21", - "mapbox-gl": "^1.13.1", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-map-gl": "^5.3.21", + "maplibre-gl": "^4.3.2", + "react": "^18.3.0", + "react-dom": "^18.3.0", + "react-map-gl": "^7.1.0", "react-router-dom": "^6.23.1", "react-spring-bottom-sheet": "^3.4.1", "react-vega": "^7.6.0", @@ -51,7 +51,6 @@ "@types/geojson": "^7946.0.8", "@types/json-stable-stringify": "^1.0.34", "@types/lodash": "^4.14.173", - "@types/mapbox-gl": "^1.13.2", "@types/node": "^20.12.12", "@types/react": "^18.3.2", "@types/react-dom": "^18.3.0", diff --git a/frontend/public/map-styles/map-style.json b/frontend/public/map-styles/map-style.json new file mode 100644 index 00000000..d2c650ec --- /dev/null +++ b/frontend/public/map-styles/map-style.json @@ -0,0 +1,3011 @@ +{ + "version": 8, + "name": "Positron and Satellite", + "metadata": {}, + "sources": { + "satellite": { + "type": "raster", + "tiles": [ + "https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2020_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.png" + ], + "tileSize": 256 + }, + "carto": { + "type": "vector", + "url": "https://tiles.basemaps.cartocdn.com/vector/carto.streets/v1/tiles.json" + } + }, + "sprite": "https://tiles.basemaps.cartocdn.com/gl/positron-gl-style/sprite", + "glyphs": "https://tiles.basemaps.cartocdn.com/fonts/{fontstack}/{range}.pbf", + "layers": [ + { + "id": "satellite", + "type": "raster", + "source": "satellite" + }, + { + "id": "background", + "type": "background", + "layout": { + "visibility": "visible" + }, + "paint": { + "background-color": "#fafaf8", + "background-opacity": 1 + } + }, + { + "id": "landcover", + "type": "fill", + "source": "carto", + "source-layer": "landcover", + "filter": [ + "any", + ["==", "class", "wood"], + ["==", "class", "grass"], + ["==", "subclass", "recreation_ground"] + ], + "paint": { + "fill-color": { + "stops": [ + [8, "rgba(234, 241, 233, 0.5)"], + [9, "rgba(234, 241, 233, 0.5)"], + [11, "rgba(234, 241, 233, 0.5)"], + [13, "rgba(234, 241, 233, 0.5)"], + [15, "rgba(234, 241, 233, 0.5)"] + ] + }, + "fill-opacity": 1 + } + }, + { + "id": "park_national_park", + "type": "fill", + "source": "carto", + "source-layer": "park", + "minzoom": 9, + "filter": ["all", ["==", "class", "national_park"]], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "stops": [ + [8, "rgba(234, 241, 233, 0.5)"], + [9, "rgba(234, 241, 233, 0.5)"], + [11, "rgba(234, 241, 233, 0.5)"], + [13, "rgba(234, 241, 233, 0.5)"], + [15, "rgba(234, 241, 233, 0.5)"] + ] + }, + "fill-opacity": 1, + "fill-translate-anchor": "map" + } + }, + { + "id": "park_nature_reserve", + "type": "fill", + "source": "carto", + "source-layer": "park", + "minzoom": 0, + "filter": ["all", ["==", "class", "nature_reserve"]], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "stops": [ + [8, "rgba(234, 241, 233, 0.5)"], + [9, "rgba(234, 241, 233, 0.5)"], + [11, "rgba(234, 241, 233, 0.5)"], + [13, "rgba(234, 241, 233, 0.5)"], + [15, "rgba(234, 241, 233, 0.5)"] + ] + }, + "fill-antialias": true, + "fill-opacity": { + "stops": [ + [6, 0.7], + [9, 0.9] + ] + } + } + }, + { + "id": "landuse_residential", + "type": "fill", + "source": "carto", + "source-layer": "landuse", + "minzoom": 6, + "filter": ["any", ["==", "class", "residential"]], + "paint": { + "fill-color": { + "stops": [ + [5, "rgba(237, 237, 237, 0.5)"], + [8, "rgba(237, 237, 237, 0.45)"], + [9, "rgba(237, 237, 237, 0.4)"], + [11, "rgba(237, 237, 237, 0.35)"], + [13, "rgba(237, 237, 237, 0.3)"], + [15, "rgba(237, 237, 237, 0.25)"], + [16, "rgba(237, 237, 237, 0.25)"] + ] + }, + "fill-opacity": { + "stops": [ + [6, 0.6], + [9, 1] + ] + } + } + }, + { + "id": "landuse", + "type": "fill", + "source": "carto", + "source-layer": "landuse", + "filter": ["any", ["==", "class", "cemetery"], ["==", "class", "stadium"]], + "paint": { + "fill-color": { + "stops": [ + [8, "rgba(234, 241, 233, 0.5)"], + [9, "rgba(234, 241, 233, 0.5)"], + [11, "rgba(234, 241, 233, 0.5)"], + [13, "rgba(234, 241, 233, 0.5)"], + [15, "rgba(234, 241, 233, 0.5)"] + ] + } + } + }, + { + "id": "waterway", + "type": "line", + "source": "carto", + "source-layer": "waterway", + "paint": { + "line-color": "#d1dbdf", + "line-width": { + "stops": [ + [8, 0.5], + [9, 1], + [15, 2], + [16, 3] + ] + } + } + }, + { + "id": "boundary_county", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 9, + "maxzoom": 24, + "filter": ["all", ["==", "admin_level", 6], ["==", "maritime", 0]], + "paint": { + "line-color": { + "stops": [ + [4, "#ead5d7"], + [5, "#ead5d7"], + [6, "#e1c5c7"] + ] + }, + "line-width": { + "stops": [ + [4, 0.5], + [7, 1] + ] + }, + "line-dasharray": { + "stops": [ + [6, [1]], + [7, [2, 2]] + ] + } + } + }, + { + "id": "boundary_state", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 4, + "filter": ["all", ["==", "admin_level", 4], ["==", "maritime", 0]], + "paint": { + "line-color": { + "stops": [ + [4, "#ead5d7"], + [5, "#ead5d7"], + [6, "#e1c5c7"] + ] + }, + "line-width": { + "stops": [ + [4, 0.5], + [7, 1], + [8, 1], + [9, 1.2] + ] + }, + "line-dasharray": { + "stops": [ + [6, [1]], + [7, [2, 2]] + ] + } + } + }, + { + "id": "water", + "type": "fill", + "source": "carto", + "source-layer": "water", + "minzoom": 0, + "maxzoom": 24, + "filter": ["all", ["==", "$type", "Polygon"]], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "#d4dadc", + "fill-antialias": true, + "fill-translate-anchor": "map", + "fill-opacity": 1 + } + }, + { + "id": "water_shadow", + "type": "fill", + "source": "carto", + "source-layer": "water", + "minzoom": 0, + "filter": ["all", ["==", "$type", "Polygon"]], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": "transparent", + "fill-antialias": true, + "fill-translate-anchor": "map", + "fill-opacity": 1, + "fill-translate": { + "stops": [ + [0, [0, 2]], + [6, [0, 1]], + [14, [0, 1]], + [17, [0, 2]] + ] + } + } + }, + { + "id": "aeroway-runway", + "type": "line", + "source": "carto", + "source-layer": "aeroway", + "minzoom": 12, + "filter": ["all", ["==", "class", "runway"]], + "layout": { + "line-cap": "square" + }, + "paint": { + "line-width": { + "stops": [ + [11, 1], + [13, 4], + [14, 6], + [15, 8], + [16, 10] + ] + }, + "line-color": "#e8e8e8" + } + }, + { + "id": "aeroway-taxiway", + "type": "line", + "source": "carto", + "source-layer": "aeroway", + "minzoom": 13, + "filter": ["all", ["==", "class", "taxiway"]], + "paint": { + "line-color": "#e8e8e8", + "line-width": { + "stops": [ + [13, 0.5], + [14, 1], + [15, 2], + [16, 4] + ] + } + } + }, + { + "id": "waterway_label", + "type": "symbol", + "source": "carto", + "source-layer": "waterway", + "filter": ["all", ["has", "name"], ["==", "class", "river"]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Regular Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "symbol-placement": "line", + "symbol-spacing": 300, + "symbol-avoid-edges": false, + "text-size": { + "stops": [ + [9, 8], + [10, 9] + ] + }, + "text-padding": 2, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-offset": { + "stops": [ + [6, [0, -0.2]], + [11, [0, -0.4]], + [12, [0, -0.6]] + ] + }, + "text-letter-spacing": 0, + "text-keep-upright": true + }, + "paint": { + "text-color": "#7a96a0", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1 + } + }, + { + "id": "tunnel_service_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "service"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 1], + [16, 3], + [17, 6], + [18, 8] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_minor_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": ["all", ["==", "class", "minor"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "butt", + "line-join": "miter" + }, + "paint": { + "line-width": { + "stops": [ + [11, 0.5], + [12, 0.5], + [14, 2], + [15, 4], + [16, 6], + [17, 10], + [18, 14] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_sec_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["in", "class", "secondary", "tertiary"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 0.5], + [12, 1], + [13, 2], + [14, 5], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_pri_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 8, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [13, 4], + [14, 6], + [15, 8], + [16, 10], + [17, 14], + [18, 18] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_trunk_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [13, 4], + [14, 6], + [15, 8], + [16, 10], + [17, 14], + [18, 18] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_mot_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + ["==", "class", "motorway"], + ["!=", "ramp", 1], + ["==", "brunnel", "tunnel"] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [12, 4], + [13, 5], + [14, 7], + [15, 9], + [16, 11], + [17, 13], + [18, 22] + ] + }, + "line-opacity": { + "stops": [ + [6, 0.5], + [7, 1] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "tunnel_path", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "path"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 0.5], + [16, 1], + [18, 3] + ] + }, + "line-opacity": 1, + "line-color": "#d5d5d5", + "line-dasharray": { + "stops": [ + [15, [2, 2]], + [18, [3, 3]] + ] + } + } + }, + { + "id": "tunnel_service_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "service"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 2], + [16, 2], + [17, 4], + [18, 6] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_minor_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "minor"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 3], + [16, 4], + [17, 8], + [18, 12] + ] + }, + "line-opacity": 1, + "line-color": "rgba(238, 238, 238, 1)" + } + }, + { + "id": "tunnel_sec_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": ["all", ["in", "class", "secondary", "tertiary"], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 2], + [13, 2], + [14, 3], + [15, 4], + [16, 6], + [17, 10], + [18, 14] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_pri_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 1], + [13, 2], + [14, 4], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_trunk_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [11, 1], + [13, 2], + [14, 4], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_mot_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + ["==", "class", "motorway"], + ["!=", "ramp", 1], + ["==", "brunnel", "tunnel"] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [10, 1], + [12, 2], + [13, 3], + [14, 5], + [15, 7], + [16, 9], + [17, 11], + [18, 20] + ] + }, + "line-opacity": 1, + "line-color": "#eee" + } + }, + { + "id": "tunnel_rail", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "tunnel"]], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#dddddd", + "line-width": { + "base": 1.3, + "stops": [ + [13, 0.5], + [14, 1], + [15, 1], + [16, 3], + [21, 7] + ] + }, + "line-opacity": 0.5 + } + }, + { + "id": "tunnel_rail_dash", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "tunnel"]], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#ffffff", + "line-width": { + "base": 1.3, + "stops": [ + [15, 0.5], + [16, 1], + [20, 5] + ] + }, + "line-dasharray": { + "stops": [ + [15, [5, 5]], + [16, [6, 6]] + ] + }, + "line-opacity": 0.5 + } + }, + { + "id": "road_service_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "service"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 1], + [16, 3], + [17, 6], + [18, 8] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "road_minor_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": ["all", ["==", "class", "minor"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 0.5], + [12, 0.5], + [14, 2], + [15, 3], + [16, 4.3], + [17, 10], + [18, 14] + ] + }, + "line-opacity": 1, + "line-opacity": 1, + "line-color": { + "stops": [ + [13, "#e6e6e6"], + [15.7, "#e6e6e6"], + [16, "#ddd"] + ] + } + } + }, + { + "id": "road_pri_case_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["==", "ramp", 1]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [12, 2], + [13, 3], + [14, 4], + [15, 5], + [16, 8], + [17, 10] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": "#ddd" + } + }, + { + "id": "road_trunk_case_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["==", "ramp", 1]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [12, 2], + [13, 3], + [14, 4], + [15, 5], + [16, 8], + [17, 10] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [12, "#e6e6e6"], + [14, "#ddd"] + ] + } + } + }, + { + "id": "road_mot_case_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [12, 2], + [13, 3], + [14, 4], + [15, 5], + [16, 8], + [17, 10] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [12, "#e6e6e6"], + [14, "#ddd"] + ] + } + } + }, + { + "id": "road_sec_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["in", "class", "secondary", "tertiary"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 0.5], + [12, 1.5], + [13, 3], + [14, 5], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [11, "#e6e6e6"], + [12.99, "#e6e6e6"], + [13, "#ddd"] + ] + } + } + }, + { + "id": "road_pri_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 7, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["!=", "ramp", 1], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [13, 4], + [14, 6], + [15, 8], + [16, 10], + [17, 14], + [18, 18] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": { + "stops": [ + [7, "#e6e6e6"], + [12, "#ddd"] + ] + } + } + }, + { + "id": "road_trunk_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["!=", "ramp", 1], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [13, 4], + [14, 6], + [15, 8], + [16, 10], + [17, 14], + [18, 18] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": { + "stops": [ + [5, "#e6e6e6"], + [12, "#ddd"] + ] + } + } + }, + { + "id": "road_mot_case_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.7], + [8, 0.8], + [11, 3], + [12, 4], + [13, 5], + [14, 7], + [15, 9], + [16, 11], + [17, 13], + [18, 22] + ] + }, + "line-opacity": { + "stops": [ + [6, 0.5], + [7, 1] + ] + }, + "line-color": { + "stops": [ + [5, "#e6e6e6"], + [12, "#ddd"] + ] + } + } + }, + { + "id": "road_path", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["in", "class", "path", "track"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 0.5], + [16, 1], + [18, 3] + ] + }, + "line-opacity": 1, + "line-color": "#d5d5d5", + "line-dasharray": { + "stops": [ + [15, [2, 2]], + [18, [3, 3]] + ] + } + } + }, + { + "id": "road_service_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "service"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 2], + [16, 2], + [17, 4], + [18, 6] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "road_minor_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "minor"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 3], + [16, 4], + [17, 8], + [18, 12] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "road_pri_fill_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["==", "ramp", 1]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [12, 1], + [13, 1.5], + [14, 2], + [15, 3], + [16, 6], + [17, 8] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_trunk_fill_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["==", "ramp", 1]], + "layout": { + "line-cap": "square", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [12, 1], + [13, 1.5], + [14, 2], + [15, 3], + [16, 6], + [17, 8] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_mot_fill_ramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 12, + "maxzoom": 24, + "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [12, 1], + [13, 1.5], + [14, 2], + [15, 3], + [16, 6], + [17, 8] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_sec_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": ["all", ["in", "class", "secondary", "tertiary"], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 2], + [13, 2], + [14, 3], + [15, 4], + [16, 6], + [17, 10], + [18, 14] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_pri_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["!=", "ramp", 1], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [10, 0.3], + [13, 2], + [14, 4], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_trunk_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["!=", "ramp", 1], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 1], + [13, 2], + [14, 4], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "road_mot_fill_noramp", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["!has", "brunnel"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [10, 1], + [12, 2], + [13, 3], + [14, 5], + [15, 7], + [16, 9], + [17, 11], + [18, 20] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "rail", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "filter": ["all", ["==", "class", "rail"], ["!=", "brunnel", "tunnel"]], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#dddddd", + "line-width": { + "base": 1.3, + "stops": [ + [13, 0.5], + [14, 1], + [15, 1], + [16, 3], + [21, 7] + ] + } + } + }, + { + "id": "rail_dash", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "filter": ["all", ["==", "class", "rail"], ["!=", "brunnel", "tunnel"]], + "layout": { + "visibility": "visible", + "line-join": "round" + }, + "paint": { + "line-color": "#ffffff", + "line-width": { + "base": 1.3, + "stops": [ + [15, 0.5], + [16, 1], + [20, 5] + ] + }, + "line-dasharray": { + "stops": [ + [15, [5, 5]], + [16, [6, 6]] + ] + } + } + }, + { + "id": "bridge_service_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "service"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 1], + [16, 3], + [17, 6], + [18, 8] + ] + }, + "line-opacity": 1, + "line-color": "#ddd" + } + }, + { + "id": "bridge_minor_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": ["all", ["==", "class", "minor"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "miter" + }, + "paint": { + "line-width": { + "stops": [ + [11, 0.5], + [12, 0.5], + [14, 2], + [15, 3], + [16, 4.3], + [17, 10], + [18, 14] + ] + }, + "line-opacity": 1, + "line-opacity": 1, + "line-color": { + "stops": [ + [13, "#e6e6e6"], + [15.7, "#e6e6e6"], + [16, "#ddd"] + ] + } + } + }, + { + "id": "bridge_sec_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["in", "class", "secondary", "tertiary"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "miter" + }, + "paint": { + "line-width": { + "stops": [ + [11, 0.5], + [12, 1.5], + [13, 3], + [14, 5], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": { + "stops": [ + [11, "#e6e6e6"], + [12.99, "#e6e6e6"], + [13, "#ddd"] + ] + } + } + }, + { + "id": "bridge_pri_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 8, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [13, 4], + [14, 6], + [15, 8], + [16, 10], + [17, 14], + [18, 18] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": { + "stops": [ + [8, "#e6e6e6"], + [12, "#ddd"] + ] + } + } + }, + { + "id": "bridge_trunk_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [13, 4], + [14, 6], + [15, 8], + [16, 10], + [17, 14], + [18, 18] + ] + }, + "line-opacity": { + "stops": [ + [5, 0.5], + [7, 1] + ] + }, + "line-color": { + "stops": [ + [5, "#e6e6e6"], + [12, "#ddd"] + ] + } + } + }, + { + "id": "bridge_mot_case", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 5, + "maxzoom": 24, + "filter": [ + "all", + ["==", "class", "motorway"], + ["!=", "ramp", 1], + ["==", "brunnel", "bridge"] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [6, 0.5], + [7, 0.8], + [8, 1], + [11, 3], + [12, 4], + [13, 5], + [14, 7], + [15, 9], + [16, 11], + [17, 13], + [18, 22] + ] + }, + "line-opacity": { + "stops": [ + [6, 0.5], + [7, 1] + ] + }, + "line-color": { + "stops": [ + [5, "#e6e6e6"], + [10, "#ddd"] + ] + } + } + }, + { + "id": "bridge_path", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "path"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 0.5], + [16, 1], + [18, 3] + ] + }, + "line-opacity": 1, + "line-color": "#d5d5d5", + "line-dasharray": { + "stops": [ + [15, [2, 2]], + [18, [3, 3]] + ] + } + } + }, + { + "id": "bridge_service_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "service"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 2], + [16, 2], + [17, 4], + [18, 6] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "bridge_minor_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 15, + "maxzoom": 24, + "filter": ["all", ["==", "class", "minor"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [15, 3], + [16, 4], + [17, 8], + [18, 12] + ] + }, + "line-opacity": 1, + "line-color": "#fdfdfd" + } + }, + { + "id": "bridge_sec_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 13, + "maxzoom": 24, + "filter": ["all", ["in", "class", "secondary", "tertiary"], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 2], + [13, 2], + [14, 3], + [15, 4], + [16, 6], + [17, 10], + [18, 14] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "bridge_pri_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["==", "class", "primary"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [11, 1], + [13, 2], + [14, 4], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "bridge_trunk_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 11, + "maxzoom": 24, + "filter": ["all", ["==", "class", "trunk"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], + "layout": { + "line-cap": "butt", + "line-join": "round", + "visibility": "visible" + }, + "paint": { + "line-width": { + "stops": [ + [11, 1], + [13, 2], + [14, 4], + [15, 6], + [16, 8], + [17, 12], + [18, 16] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "bridge_mot_fill", + "type": "line", + "source": "carto", + "source-layer": "transportation", + "minzoom": 10, + "maxzoom": 24, + "filter": [ + "all", + ["==", "class", "motorway"], + ["!=", "ramp", 1], + ["==", "brunnel", "bridge"] + ], + "layout": { + "line-cap": "butt", + "line-join": "round" + }, + "paint": { + "line-width": { + "stops": [ + [10, 1], + [12, 2], + [13, 3], + [14, 5], + [15, 7], + [16, 9], + [17, 11], + [18, 20] + ] + }, + "line-opacity": 1, + "line-color": "#fff" + } + }, + { + "id": "building", + "type": "fill", + "source": "carto", + "source-layer": "building", + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-color": { + "base": 1, + "stops": [ + [15.5, "#dfdfdf"], + [16, "#dfdfdf"] + ] + }, + "fill-antialias": true + } + }, + { + "id": "building-top", + "type": "fill", + "source": "carto", + "source-layer": "building", + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-translate": { + "base": 1, + "stops": [ + [14, [0, 0]], + [16, [-2, -2]] + ] + }, + "fill-outline-color": "#dfdfdf", + "fill-color": "#ededed", + "fill-opacity": { + "base": 1, + "stops": [ + [13, 0], + [16, 1] + ] + } + } + }, + { + "id": "boundary_country_outline", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 6, + "maxzoom": 24, + "filter": ["all", ["==", "admin_level", 2], ["==", "maritime", 0]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": "#f3efed", + "line-opacity": 0.5, + "line-width": 8, + "line-offset": 0 + } + }, + { + "id": "boundary_country_inner", + "type": "line", + "source": "carto", + "source-layer": "boundary", + "minzoom": 0, + "filter": ["all", ["==", "admin_level", 2], ["==", "maritime", 0]], + "layout": { + "line-cap": "round", + "line-join": "round" + }, + "paint": { + "line-color": { + "stops": [ + [4, "#f2e6e7"], + [5, "#ebd6d8"], + [6, "#ebd6d8"] + ] + }, + "line-opacity": 1, + "line-width": { + "stops": [ + [3, 1], + [6, 1.5] + ] + }, + "line-offset": 0 + } + }, + { + "id": "watername_ocean", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "minzoom": 0, + "maxzoom": 5, + "filter": ["all", ["has", "name"], ["==", "$type", "Point"], ["==", "class", "ocean"]], + "layout": { + "text-field": "{name}", + "symbol-placement": "point", + "text-size": { + "stops": [ + [0, 13], + [2, 14], + [4, 18] + ] + }, + "text-font": [ + "Montserrat Medium Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-line-height": 1.2, + "text-padding": 2, + "text-allow-overlap": false, + "text-ignore-placement": false, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-max-width": 6, + "text-letter-spacing": 0.1 + }, + "paint": { + "text-color": "#abb6be", + "text-halo-color": "#d4dadc", + "text-halo-width": 1, + "text-halo-blur": 0 + } + }, + { + "id": "watername_sea", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "minzoom": 5, + "filter": ["all", ["has", "name"], ["==", "$type", "Point"], ["==", "class", "sea"]], + "layout": { + "text-field": "{name}", + "symbol-placement": "point", + "text-size": 12, + "text-font": [ + "Montserrat Medium Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-line-height": 1.2, + "text-padding": 2, + "text-allow-overlap": false, + "text-ignore-placement": false, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-max-width": 6, + "text-letter-spacing": 0.1 + }, + "paint": { + "text-color": "#abb6be", + "text-halo-color": "#d4dadc", + "text-halo-width": 1, + "text-halo-blur": 0 + } + }, + { + "id": "watername_lake", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "minzoom": 4, + "filter": ["all", ["has", "name"], ["==", "$type", "Point"], ["==", "class", "lake"]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "symbol-placement": "point", + "text-size": { + "stops": [ + [13, 9], + [14, 10], + [15, 11], + [16, 12], + [17, 13] + ] + }, + "text-font": [ + "Montserrat Regular Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-line-height": 1.2, + "text-padding": 2, + "text-allow-overlap": false, + "text-ignore-placement": false, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto" + }, + "paint": { + "text-color": "#7a96a0", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "watername_lake_line", + "type": "symbol", + "source": "carto", + "source-layer": "water_name", + "filter": ["all", ["has", "name"], ["==", "$type", "LineString"]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "symbol-placement": "line", + "text-size": { + "stops": [ + [13, 9], + [14, 10], + [15, 11], + [16, 12], + [17, 13] + ] + }, + "text-font": [ + "Montserrat Regular Italic", + "Open Sans Italic", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "symbol-spacing": 350, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-line-height": 1.2 + }, + "paint": { + "text-color": "#7a96a0", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1, + "text-halo-blur": 1 + } + }, + { + "id": "place_hamlet", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 12, + "maxzoom": 16, + "filter": ["any", ["==", "class", "neighbourhood"], ["==", "class", "hamlet"]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [14, "{name}"] + ] + }, + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [13, 8], + [14, 10], + [16, 11] + ] + }, + "icon-image": "", + "icon-offset": [16, 0], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": { + "stops": [ + [12, "none"], + [14, "uppercase"] + ] + } + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_suburbs", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 12, + "maxzoom": 16, + "filter": ["all", ["==", "class", "suburb"]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [12, 9], + [13, 10], + [14, 11], + [15, 12], + [16, 13] + ] + }, + "icon-image": "", + "icon-offset": [16, 0], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": { + "stops": [ + [8, "none"], + [12, "uppercase"] + ] + } + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_villages", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 10, + "maxzoom": 16, + "filter": ["all", ["==", "class", "village"]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [10, 9], + [12, 10], + [13, 11], + [14, 12], + [16, 13] + ] + }, + "icon-image": "", + "icon-offset": [16, 0], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": "none" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_town", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 8, + "maxzoom": 14, + "filter": ["all", ["==", "class", "town"]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [8, 10], + [9, 10], + [10, 11], + [13, 14], + [14, 15] + ] + }, + "icon-image": "", + "icon-offset": [16, 0], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": "none" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_country_2", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 3, + "maxzoom": 10, + "filter": ["all", ["==", "class", "country"], [">=", "rank", 3], ["has", "iso_a2"]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [3, 10], + [5, 11], + [6, 12], + [7, 13], + [8, 14] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": { + "stops": [ + [3, "#8a99a4"], + [5, "#a1adb6"], + [6, "#b9c2c9"] + ] + }, + "text-halo-color": "#fafaf8", + "text-halo-width": 1 + } + }, + { + "id": "place_country_1", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 2, + "maxzoom": 7, + "filter": ["all", ["==", "class", "country"], ["<=", "rank", 2]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [3, 11], + [4, 12], + [5, 13], + [6, 14] + ] + }, + "text-transform": "uppercase", + "text-max-width": { + "stops": [ + [2, 6], + [3, 6], + [4, 9], + [5, 12] + ] + } + }, + "paint": { + "text-color": { + "stops": [ + [3, "#8a99a4"], + [5, "#a1adb6"], + [6, "#b9c2c9"] + ] + }, + "text-halo-color": "#fafaf8", + "text-halo-width": 1 + } + }, + { + "id": "place_state", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 5, + "maxzoom": 10, + "filter": ["all", ["==", "class", "state"], ["<=", "rank", 4]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [5, 12], + [7, 14] + ] + }, + "text-transform": "uppercase", + "text-max-width": 9 + }, + "paint": { + "text-color": "#97a4ae", + "text-halo-color": "#fafaf8", + "text-halo-width": 0 + } + }, + { + "id": "place_continent", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 0, + "maxzoom": 2, + "filter": ["all", ["==", "class", "continent"]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-transform": "uppercase", + "text-size": 14, + "text-letter-spacing": 0.1, + "text-max-width": 9, + "text-justify": "center", + "text-keep-upright": false + }, + "paint": { + "text-color": "#697b89", + "text-halo-color": "#fafaf8", + "text-halo-width": 1 + } + }, + { + "id": "place_city_r6", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 8, + "maxzoom": 15, + "filter": ["all", ["==", "class", "city"], [">=", "rank", 6]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [8, 12], + [9, 13], + [10, 14], + [13, 17], + [14, 20] + ] + }, + "icon-image": "", + "icon-offset": [16, 0], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_r5", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 8, + "maxzoom": 15, + "filter": ["all", ["==", "class", "city"], [">=", "rank", 0], ["<=", "rank", 5]], + "layout": { + "text-field": { + "stops": [ + [8, "{name_en}"], + [13, "{name}"] + ] + }, + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [8, 14], + [10, 16], + [13, 19], + [14, 22] + ] + }, + "icon-image": "", + "icon-offset": [16, 0], + "text-anchor": "center", + "icon-size": 1, + "text-max-width": 10, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_r7", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 6, + "maxzoom": 7, + "filter": ["all", ["==", "class", "city"], ["<=", "rank", 7]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [16, 5], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [0.2, 0.2] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_r4", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 5, + "maxzoom": 7, + "filter": ["all", ["==", "class", "city"], ["<=", "rank", 4]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [16, 5], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [0.2, 0.2] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_r2", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 4, + "maxzoom": 7, + "filter": ["all", ["==", "class", "city"], ["<=", "rank", 2]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [16, 5], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [0.2, 0.2] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_city_dot_z7", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 7, + "maxzoom": 8, + "filter": ["all", ["!has", "capital"], ["!in", "class", "country", "state"]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [16, 5], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [0.2, 0.2] + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "place_capital_dot_z7", + "type": "symbol", + "source": "carto", + "source-layer": "place", + "minzoom": 7, + "maxzoom": 8, + "filter": ["all", [">", "capital", 0]], + "layout": { + "text-field": "{name_en}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 12, + "icon-image": "circle-11", + "icon-offset": [16, 5], + "text-anchor": "right", + "icon-size": 0.4, + "text-max-width": 8, + "text-keep-upright": true, + "text-offset": [0.2, 0.2], + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#697b89", + "icon-color": "#697b89", + "icon-translate-anchor": "map", + "text-halo-color": "rgba(255,255,255,0.5)", + "text-halo-width": 1 + } + }, + { + "id": "poi_stadium", + "type": "symbol", + "source": "carto", + "source-layer": "poi", + "minzoom": 15, + "filter": ["all", ["in", "class", "stadium", "cemetery", "attraction"], ["<=", "rank", 3]], + "layout": { + "text-field": "{name}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [15, 8], + [17, 9], + [18, 10] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#7d9c83", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1 + } + }, + { + "id": "poi_park", + "type": "symbol", + "source": "carto", + "source-layer": "poi", + "minzoom": 15, + "filter": ["all", ["==", "class", "park"]], + "layout": { + "text-field": "{name}", + "text-font": [ + "Montserrat Medium", + "Open Sans Bold", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [15, 8], + [17, 9], + [18, 10] + ] + }, + "text-transform": "uppercase" + }, + "paint": { + "text-color": "#7d9c83", + "text-halo-color": "#f5f5f3", + "text-halo-width": 1 + } + }, + { + "id": "roadname_minor", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 16, + "filter": ["all", ["in", "class", "minor", "service"]], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": 9, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": 200, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center" + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "roadname_sec", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 15, + "filter": ["all", ["in", "class", "secondary", "tertiary"]], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [15, 9], + [16, 11], + [18, 12] + ] + }, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": 200, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center" + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "roadname_pri", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 14, + "filter": ["all", ["in", "class", "primary"]], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [14, 10], + [15, 10], + [16, 11], + [18, 12] + ] + }, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": { + "stops": [ + [6, 200], + [16, 250] + ] + }, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center", + "text-letter-spacing": { + "stops": [ + [14, 0], + [16, 0.2] + ] + } + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "roadname_major", + "type": "symbol", + "source": "carto", + "source-layer": "transportation_name", + "minzoom": 13, + "filter": ["all", ["in", "class", "trunk", "motorway"]], + "layout": { + "symbol-placement": "line", + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ], + "text-size": { + "stops": [ + [14, 10], + [15, 10], + [16, 11], + [18, 12] + ] + }, + "text-field": "{name}", + "symbol-avoid-edges": false, + "symbol-spacing": { + "stops": [ + [6, 200], + [16, 250] + ] + }, + "text-pitch-alignment": "auto", + "text-rotation-alignment": "auto", + "text-justify": "center", + "text-letter-spacing": { + "stops": [ + [13, 0], + [16, 0.2] + ] + } + }, + "paint": { + "text-color": "#838383", + "text-halo-color": "#fff", + "text-halo-width": 1 + } + }, + { + "id": "housenumber", + "type": "symbol", + "source": "carto", + "source-layer": "housenumber", + "minzoom": 17, + "maxzoom": 24, + "layout": { + "text-field": "{housenumber}", + "text-size": { + "stops": [ + [17, 9], + [18, 11] + ] + }, + "text-font": [ + "Montserrat Regular", + "Open Sans Regular", + "Noto Sans Regular", + "HanWangHeiLight Regular", + "NanumBarunGothic Regular" + ] + }, + "paint": { + "text-halo-color": "transparent", + "text-color": "transparent", + "text-halo-width": 0.75 + } + } + ], + "id": "voyager", + "owner": "Carto" +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6905f336..e28a8fb7 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,7 +12,7 @@ import { Nav, NavItemConfig } from './Nav'; import 'react-spring-bottom-sheet/dist/style.css'; import './index.css'; -import 'mapbox-gl/dist/mapbox-gl.css'; +import 'maplibre-gl/dist/maplibre-gl.css'; import { Notice } from 'Notice'; import { RecoilLocalStorageSync } from 'lib/recoil/sync-stores/RecoilLocalStorageSync'; diff --git a/frontend/src/config/basemaps.ts b/frontend/src/config/basemaps.ts new file mode 100644 index 00000000..9883c74b --- /dev/null +++ b/frontend/src/config/basemaps.ts @@ -0,0 +1,128 @@ +import { makeConfig } from '../lib/helpers'; + +export interface BackgroundSpecification { + id: string; + label: string; + layers: string[]; +} + +export const BASEMAP_STYLE_URL = '/map-styles/map-style.json'; + +export const BACKGROUNDS = makeConfig([ + { + id: 'light', + label: 'Map', + layers: [ + 'background', + 'landcover', + 'park_national_park', + 'park_nature_reserve', + 'landuse_residential', + 'landuse', + 'waterway', + 'boundary_county', + 'boundary_state', + 'water', + 'water_shadow', + 'aeroway-runway', + 'aeroway-taxiway', + 'waterway_label', + 'tunnel_service_case', + 'tunnel_minor_case', + 'tunnel_sec_case', + 'tunnel_pri_case', + 'tunnel_trunk_case', + 'tunnel_mot_case', + 'tunnel_path', + 'tunnel_service_fill', + 'tunnel_minor_fill', + 'tunnel_sec_fill', + 'tunnel_pri_fill', + 'tunnel_trunk_fill', + 'tunnel_mot_fill', + 'tunnel_rail', + 'tunnel_rail_dash', + 'road_service_case', + 'road_minor_case', + 'road_pri_case_ramp', + 'road_trunk_case_ramp', + 'road_mot_case_ramp', + 'road_sec_case_noramp', + 'road_pri_case_noramp', + 'road_trunk_case_noramp', + 'road_mot_case_noramp', + 'road_path', + 'road_service_fill', + 'road_minor_fill', + 'road_pri_fill_ramp', + 'road_trunk_fill_ramp', + 'road_mot_fill_ramp', + 'road_sec_fill_noramp', + 'road_pri_fill_noramp', + 'road_trunk_fill_noramp', + 'road_mot_fill_noramp', + 'rail', + 'rail_dash', + 'bridge_service_case', + 'bridge_minor_case', + 'bridge_sec_case', + 'bridge_pri_case', + 'bridge_trunk_case', + 'bridge_mot_case', + 'bridge_path', + 'bridge_service_fill', + 'bridge_minor_fill', + 'bridge_sec_fill', + 'bridge_pri_fill', + 'bridge_trunk_fill', + 'bridge_mot_fill', + 'building', + 'building-top', + 'boundary_country_outline', + 'boundary_country_inner', + ], + }, + { + id: 'satellite', + label: 'Satellite', + layers: ['satellite'], + }, +]); + +export const BACKGROUND_ATTRIBUTIONS: Record = { + light: + 'Background map data © OpenStreetMap contributors, style © CARTO', + satellite: + 'Satellite imagery: Sentinel-2 cloudless - https://s2maps.eu by EOX IT Services GmbH (Contains modified Copernicus Sentinel data 2020)', +}; + +export type BackgroundName = keyof typeof BACKGROUNDS; + +export const LABELS_LAYERS = [ + 'watername_ocean', + 'watername_sea', + 'watername_lake', + 'watername_lake_line', + 'place_hamlet', + 'place_suburbs', + 'place_villages', + 'place_town', + 'place_country_2', + 'place_country_1', + 'place_state', + 'place_continent', + 'place_city_r6', + 'place_city_r5', + 'place_city_dot_r7', + 'place_city_dot_r4', + 'place_city_dot_r2', + 'place_city_dot_z7', + 'place_capital_dot_z7', + 'poi_stadium', + 'poi_park', + 'roadname_minor', + 'roadname_sec', + 'roadname_pri', + 'roadname_major', + 'housenumber', +]; diff --git a/frontend/src/config/map-view.ts b/frontend/src/config/map-view.ts index 451ca666..3cd921dc 100644 --- a/frontend/src/config/map-view.ts +++ b/frontend/src/config/map-view.ts @@ -20,7 +20,7 @@ export const mapViewConfig: MapViewConfig = { }, viewLimits: { minZoom: 3, - maxZoom: 16, + maxZoom: 12, maxPitch: 0, }, }; diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index ffc54a78..46b0f12b 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,6 +1,5 @@ -import { createRoot } from 'react-dom/client'; +import { render } from 'react-dom'; import { App } from './App'; const container = document.getElementById('root'); -const root = createRoot(container); -root.render(); +render(, container); diff --git a/frontend/src/lib/data-map/BaseMap.tsx b/frontend/src/lib/data-map/BaseMap.tsx new file mode 100644 index 00000000..93a98096 --- /dev/null +++ b/frontend/src/lib/data-map/BaseMap.tsx @@ -0,0 +1,34 @@ +import { MapViewState } from 'deck.gl/typed'; +import { ComponentProps, FC, ReactNode } from 'react'; +import { Map } from 'react-map-gl/maplibre'; + +export interface BaseMapProps { + mapStyle: ComponentProps['mapStyle']; + viewState: MapViewState; + onViewState: (vs: MapViewState) => void; + children?: ReactNode; +} + +/** + * Displays a react-map-gl basemap component. + * Accepts children such as a DeckGLOverlay, HUD controls etc + */ +export const BaseMap: FC = ({ mapStyle, viewState, onViewState, children }) => { + return ( + onViewState(viewState)} + mapStyle={mapStyle} + dragRotate={false} + keyboard={false} + touchZoomRotate={true} + touchPitch={false} + antialias={true} + attributionControl={false} + > + {children} + + ); +}; diff --git a/frontend/src/lib/data-map/DataMap.tsx b/frontend/src/lib/data-map/DataMap.tsx index 61ec4269..ccaed85e 100644 --- a/frontend/src/lib/data-map/DataMap.tsx +++ b/frontend/src/lib/data-map/DataMap.tsx @@ -1,20 +1,19 @@ -import difference from 'lodash/difference'; -import { FC, ReactNode, useCallback, useEffect, useMemo } from 'react'; +import type { MapboxOverlay } from '@deck.gl/mapbox/typed'; +import { useMap } from 'react-map-gl/maplibre'; +import { FC, useCallback, useMemo, useRef } from 'react'; -import { usePrevious } from '../hooks/use-previous'; -import { useTrackingRef } from '../hooks/use-tracking-ref'; -import { useTrigger } from '../hooks/use-trigger'; +import { useTriggerMemo } from '../hooks/use-trigger-memo'; +import { useDataLoadTrigger } from './use-data-load-trigger'; -import { DeckMap } from './DeckMap'; +import { DeckGLOverlay } from '../map/DeckGLOverlay'; import { useInteractions } from './interactions/use-interactions'; import { ViewLayer, ViewLayerParams } from './view-layers'; export interface DataMapProps { - initialViewState: any; + beforeId: string, viewLayers: ViewLayer[]; viewLayersParams: Record; interactionGroups: any; - children?: ReactNode; } // set a convention where the view layer id is either the first part of the deck id before the @ sign, or it's the whole id @@ -23,17 +22,14 @@ function lookupViewForDeck(deckLayerId: string) { } export const DataMap: FC = ({ - initialViewState, + beforeId, viewLayers, viewLayersParams, interactionGroups, - children, }) => { - const { onHover, onClick, layerFilter, pickingRadius } = useInteractions( - viewLayers, - lookupViewForDeck, - interactionGroups, - ); + const deckRef = useRef(); + const { current: map } = useMap(); + const zoom = map.getMap().getZoom(); const dataLoaders = useMemo( () => @@ -43,66 +39,54 @@ export const DataMap: FC = ({ [viewLayers], ); - const [dataLoadTrigger, triggerDataUpdate] = useTrigger(); - - const doTrigger = useCallback(() => { - triggerDataUpdate(); - }, [triggerDataUpdate]); - - const previousLoaders = usePrevious(dataLoaders); - - useEffect(() => { - // destroy removed data loaders to free up memory - const removedLoaders = difference(previousLoaders ?? [], dataLoaders); - removedLoaders.forEach((dl) => dl.destroy()); - - // subscribe to new data loaders to get notified when data is loaded - const addedLoaders = difference(dataLoaders, previousLoaders ?? []); - addedLoaders.forEach((dl) => dl.subscribe(doTrigger)); + const dataLoadTrigger = useDataLoadTrigger(dataLoaders); - // if there was a change in data loaders, trigger an update to the data map - if (addedLoaders.length > 0 || removedLoaders.length > 0) { - doTrigger(); - } - }, [dataLoaders, previousLoaders, doTrigger]); - /* store current value of dataLoaders so that we can clean up data on component unmount - * this is necessary because we don't want to keep the data loaders around after the component is unmounted - */ - const currentLoadersRef = useTrackingRef(dataLoaders); - useEffect(() => { - return () => { - // eslint-disable-next-line react-hooks/exhaustive-deps - currentLoadersRef.current?.forEach((dl) => dl.destroy()); - }; - }, [currentLoadersRef]); - - const deckLayersFunction = useCallback( + const layersFunction = useCallback( ({ zoom }: { zoom: number }) => viewLayers.map((viewLayer) => - makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom), + makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom, beforeId), ), - [viewLayers, viewLayersParams], + [beforeId, viewLayers, viewLayersParams], + ); + + const { onHover, onClick, layerFilter, pickingRadius } = useInteractions( + viewLayers, + lookupViewForDeck, + interactionGroups, + ); + + const layers = useTriggerMemo( + () => layersFunction({ zoom }), + [layersFunction, zoom], + dataLoadTrigger, ); return ( - 'default'} + layers={layers} + layerFilter={layerFilter} + onHover={(info) => deckRef.current && onHover?.(info, deckRef.current)} + onClick={(info) => deckRef.current && onClick?.(info, deckRef.current)} pickingRadius={pickingRadius} - > - {children} - + /> ); }; -function makeDeckLayers(viewLayer: ViewLayer, viewLayerParams: ViewLayerParams, zoom: number) { +function makeDeckLayers( + viewLayer: ViewLayer, + viewLayerParams: ViewLayerParams, + zoom: number, + beforeId: string | undefined, +) { return viewLayer.fn({ - deckProps: { id: viewLayer.id, pickable: !!viewLayer.interactionGroup }, + deckProps: { id: viewLayer.id, pickable: !!viewLayer.interactionGroup, beforeId }, zoom, ...viewLayerParams, }); diff --git a/frontend/src/lib/data-map/DeckMap.tsx b/frontend/src/lib/data-map/DeckMap.tsx deleted file mode 100644 index 3b1bd702..00000000 --- a/frontend/src/lib/data-map/DeckMap.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import DeckGL, { - DeckGLContextValue, - DeckGLRef, - DeckProps, - MapView, - MapViewState, -} from 'deck.gl/typed'; -import { FC, Provider, ReactNode, createContext, useRef, useState } from 'react'; - -import { useTriggerMemo } from '../hooks/use-trigger-memo'; -import { MapContextProviderWithLimits } from './MapContextProviderWithLimits'; - -interface DeckMapProps { - initialViewState: any; - layersFunction: ({ zoom }) => any[]; - dataLoadTrigger?: number; - onHover: any; - onClick?: any; - layerRenderFilter: DeckProps['layerFilter']; - pickingRadius?: number; - children: ReactNode; -} - -export const ViewStateContext = createContext<{ - viewState: MapViewState; - setViewState: (viewState: MapViewState) => void; -}>(null); - -export const DeckMap: FC = ({ - initialViewState, - layersFunction, - dataLoadTrigger, - onHover, - onClick, - layerRenderFilter, - pickingRadius, - children, -}) => { - const [viewState, setViewState] = useState(initialViewState); - - const deckRef = useRef(); - - const zoom = viewState.zoom; - - const layers = useTriggerMemo( - () => layersFunction({ zoom }), - [layersFunction, zoom], - dataLoadTrigger, - ); - - return ( - - 'default'} - views={[ - new MapView({ - repeat: true, - controller: { - scrollZoom: { - smooth: true, - speed: 0.2, - }, - keyboard: false, //can't deactivate keyboard rotate only so deactivate all keyboard - dragRotate: false, - touchRotate: false, - }, - }), - ]} - viewState={viewState} - onViewStateChange={({ viewState }) => setViewState(viewState)} - layers={layers} - layerFilter={layerRenderFilter} - onHover={(info, event) => - !event.srcEvent.defaultPrevented && // ignore pointer events from HUD: https://github.com/visgl/deck.gl/discussions/6252 - deckRef.current && - onHover(info, deckRef.current) - } - onClick={(info, event) => - !event.srcEvent.defaultPrevented && // ignore pointer events from HUD: https://github.com/visgl/deck.gl/discussions/6252 - deckRef.current && - onClick?.(info, deckRef.current) - } - pickingRadius={pickingRadius} - ContextProvider={ - MapContextProviderWithLimits as unknown as Provider /* unknown because TS doesn't like the cast */ - } - > - {/* make sure components like StaticMap are immediate children of DeckGL so that they - can be managed properly by Deck - see https://deck.gl/docs/api-reference/react/deckgl#jsx-layers - */} - {children} - - - ); -}; diff --git a/frontend/src/lib/data-map/MapContextProviderWithLimits.tsx b/frontend/src/lib/data-map/MapContextProviderWithLimits.tsx deleted file mode 100644 index dc756bf7..00000000 --- a/frontend/src/lib/data-map/MapContextProviderWithLimits.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import omit from 'lodash/omit'; -import { FC, ReactNode, useContext, useMemo } from 'react'; -import { MapContext, MapContextProps } from 'react-map-gl'; - -import { ViewStateContext } from './DeckMap'; - -/** - * Needed to add the missing view state limits (maxZoom etc) to the react-map-gl MapContext. - * - * Deck.gl doesn't forward these properties to the context it creates, - * so react-map-gl components like NavigationControl end up resetting the viewLimits to defaults. - * - * CAUTION: after this extension, the viewport is no longer a proper deck.gl WebMercatorViewport. - * It's just a plain object with the properties copied over, but it has no prototype attached. - * So users of the context can't use viewport.fitBounds(...) etc. - * This was a side effect of deck passing its own viewport into the MapContext, anyway. - */ -export const MapContextProviderWithLimits: FC<{ - children: ReactNode; - value: MapContextProps; -}> = ({ value, children }) => { - const baseContext = value; - const { - viewState: { minPitch, maxPitch, minZoom, maxZoom }, - } = useContext(ViewStateContext); - - // need to make a plain object out of viewport, and then assign the missing fields - const plainViewport = Object.fromEntries(Object.entries(baseContext.viewport ?? {})); - - const extendedContext = useMemo( - () => ({ - ...omit(baseContext, 'viewport'), - viewport: { ...plainViewport, minPitch, maxPitch, minZoom, maxZoom } as any, - }), - [baseContext, plainViewport, minPitch, maxPitch, minZoom, maxZoom], - ); - return {children}; -}; diff --git a/frontend/src/lib/data-map/ViewStateDebug.tsx b/frontend/src/lib/data-map/ViewStateDebug.tsx deleted file mode 100644 index 2c9c99df..00000000 --- a/frontend/src/lib/data-map/ViewStateDebug.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { useContext } from 'react'; - -import { ViewStateContext } from 'lib/data-map/DeckMap'; -import { Box, Paper } from '@mui/material'; - -export const ViewStateDebug = () => { - const { viewState } = useContext(ViewStateContext); - - return ( - - -

Zoom: {viewState.zoom.toFixed(2)}

-

Lat: {viewState.latitude.toFixed(4)}

-

Lon: {viewState.longitude.toFixed(4)}

-
-
- ); -}; diff --git a/frontend/src/lib/data-map/use-data-load-trigger.ts b/frontend/src/lib/data-map/use-data-load-trigger.ts new file mode 100644 index 00000000..1ea34c50 --- /dev/null +++ b/frontend/src/lib/data-map/use-data-load-trigger.ts @@ -0,0 +1,46 @@ +import difference from 'lodash/difference'; +import { useEffect } from 'react'; + +import { DataLoader } from '../data-loader/data-loader'; +import { usePrevious } from '../hooks/use-previous'; +import { useTrackingRef } from '../hooks/use-tracking-ref'; +import { useTrigger } from '../hooks/use-trigger'; + +/** + * Based on a list of data loaders extracted from view layers, + * returns a number which can be used as a trigger to recalculate a list of deck layers. + * + */ +export function useDataLoadTrigger(dataLoaders: DataLoader[]) { + const [dataLoadTrigger, doTriggerDataUpdate] = useTrigger(); + + const previousLoaders = usePrevious(dataLoaders); + + useEffect(() => { + // destroy removed data loaders to free up memory + const removedLoaders = difference(previousLoaders ?? [], dataLoaders); + removedLoaders.forEach((dl) => dl.destroy()); + + // subscribe to new data loaders to get notified when data is loaded + const addedLoaders = difference(dataLoaders, previousLoaders ?? []); + addedLoaders.forEach((dl) => dl.subscribe(doTriggerDataUpdate)); + + // if there was a change in data loaders, trigger an update to the data map + if (addedLoaders.length > 0 || removedLoaders.length > 0) { + doTriggerDataUpdate(); + } + }, [dataLoaders, previousLoaders, doTriggerDataUpdate]); + + /* store current value of dataLoaders so that we can clean up data on component unmount + * this is necessary because we don't want to keep the data loaders around after the component is unmounted + */ + const currentLoadersRef = useTrackingRef(dataLoaders); + useEffect(() => { + return () => { + // eslint-disable-next-line react-hooks/exhaustive-deps + currentLoadersRef.current?.forEach((dl) => dl.destroy()); + }; + }, [currentLoadersRef]); + + return dataLoadTrigger; +} diff --git a/frontend/src/lib/deck/layers/data-loader-layer.ts b/frontend/src/lib/deck/layers/data-loader-layer.ts index db3726bd..6917f311 100644 --- a/frontend/src/lib/deck/layers/data-loader-layer.ts +++ b/frontend/src/lib/deck/layers/data-loader-layer.ts @@ -1,5 +1,5 @@ import { DataLoader } from 'lib/data-loader/data-loader'; -import { MapboxGeoJSONFeature } from 'mapbox-gl'; +import { MapGeoJSONFeature } from 'maplibre-gl'; export interface DataLoaderOptions { dataLoader: DataLoader; @@ -10,7 +10,7 @@ export function dataLoaderLayer(tileProps, { dataLoader }: DataLoaderOptions) { tile: { content }, } = tileProps; if (content && dataLoader) { - const ids: number[] = content.map((f: MapboxGeoJSONFeature) => f.id); + const ids: number[] = content.map((f: MapGeoJSONFeature) => f.id); dataLoader.loadDataForIds(ids); } diff --git a/frontend/src/lib/deck/props/data-source.ts b/frontend/src/lib/deck/props/data-source.ts index a77e7363..77e178ee 100644 --- a/frontend/src/lib/deck/props/data-source.ts +++ b/frontend/src/lib/deck/props/data-source.ts @@ -1,12 +1,12 @@ import memoize from 'lodash/memoize'; -import { MapboxGeoJSONFeature } from 'mapbox-gl'; +import { MapGeoJSONFeature } from 'maplibre-gl'; import { DataLoader } from 'lib/data-loader/data-loader'; import { Accessor, withLoaderTriggers, withTriggers } from './getters'; export const featureProperty = memoize( - (field: string | Accessor): Accessor => { + (field: string | Accessor): Accessor => { return typeof field === 'string' ? withTriggers((f) => f.properties[field], [field]) : field; }, ); diff --git a/frontend/src/lib/hooks/use-throttled-callback.ts b/frontend/src/lib/hooks/use-throttled-callback.ts new file mode 100644 index 00000000..f1c06ce7 --- /dev/null +++ b/frontend/src/lib/hooks/use-throttled-callback.ts @@ -0,0 +1,33 @@ +import throttle from 'lodash/throttle'; +import { useEffect, useMemo } from 'react'; + +import { useTrackingRef } from './use-tracking-ref'; + +export function useThrottledCallback( + callback: (...args: Args) => void, + ms: number, + leading: boolean = false, + trailing: boolean = true, +) { + const callbackRef = useTrackingRef(callback); + + const throttledHandler = useMemo( + () => + throttle( + (...args: Args) => { + callbackRef.current(...args); + }, + ms, + { leading, trailing }, + ), + [ms, callbackRef, leading, trailing], + ); + + useEffect(() => { + return () => { + throttledHandler.cancel(); + }; + }, [throttledHandler]); + + return throttledHandler; +} diff --git a/frontend/src/lib/map/DeckGLOverlay.tsx b/frontend/src/lib/map/DeckGLOverlay.tsx new file mode 100644 index 00000000..397ed221 --- /dev/null +++ b/frontend/src/lib/map/DeckGLOverlay.tsx @@ -0,0 +1,14 @@ +import { MapboxOverlay, MapboxOverlayProps } from '@deck.gl/mapbox/typed'; +import { forwardRef, useImperativeHandle } from 'react'; +import { useControl } from 'react-map-gl/maplibre'; + +type DeckGLOverlayProps = MapboxOverlayProps; + +export const DeckGLOverlay = forwardRef((props, ref) => { + const overlay = useControl(() => new MapboxOverlay(props)); + overlay.setProps(props); + + useImperativeHandle(ref, () => overlay); + + return null; +}); diff --git a/frontend/src/lib/map/MapBoundsFitter.tsx b/frontend/src/lib/map/MapBoundsFitter.tsx index aa23db3f..25b08827 100644 --- a/frontend/src/lib/map/MapBoundsFitter.tsx +++ b/frontend/src/lib/map/MapBoundsFitter.tsx @@ -1,37 +1,21 @@ -import { easeCubic } from 'd3-ease'; -import { FC, useContext } from 'react'; -import { FlyToInterpolator, WebMercatorViewport } from 'react-map-gl'; +import { WebMercatorViewport } from 'deck.gl/typed'; +import { FC, useEffect } from 'react'; +import { useMap } from 'react-map-gl/maplibre'; -import { BoundingBox, appToDeckBoundingBox } from 'lib/bounding-box'; -import { ViewStateContext } from 'lib/data-map/DeckMap'; -import { useChangeEffect } from 'lib/hooks/use-change-effect'; +import { BoundingBox, appToDeckBoundingBox } from '../bounding-box'; interface MapBoundsFitterProps { boundingBox: BoundingBox; } export const MapBoundsFitter: FC = ({ boundingBox }) => { - const { viewState, setViewState } = useContext(ViewStateContext); + const { current: map } = useMap(); - useChangeEffect( - () => { - if (boundingBox != null) { - const { latitude, longitude, zoom } = getBoundingBoxViewState(boundingBox); - - setViewState({ - ...viewState, - latitude, - longitude, - zoom, - transitionDuration: 1500, - transitionInterpolator: new FlyToInterpolator() as any, - transitionEasing: easeCubic, - }); - } - }, - [boundingBox, viewState, setViewState], - [boundingBox], - ); + useEffect(() => { + if (boundingBox != null && map != null) { + map.fitBounds(boundingBox, {}); + } + }, [boundingBox, map]); return null; }; @@ -46,4 +30,4 @@ export function getBoundingBoxViewState( const { latitude, longitude, zoom } = viewport.fitBounds(deckBbox, { padding: 20 }); return { latitude, longitude, zoom }; -} +} \ No newline at end of file diff --git a/frontend/src/lib/map/hud/mapbox-controls.tsx b/frontend/src/lib/map/hud/map-controls.tsx similarity index 100% rename from frontend/src/lib/map/hud/mapbox-controls.tsx rename to frontend/src/lib/map/hud/map-controls.tsx diff --git a/frontend/src/lib/recoil/sync-state-throttled.ts b/frontend/src/lib/recoil/sync-state-throttled.ts new file mode 100644 index 00000000..a4c33ede --- /dev/null +++ b/frontend/src/lib/recoil/sync-state-throttled.ts @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { RecoilState, RecoilValueReadOnly, useRecoilValue, useSetRecoilState } from 'recoil'; + +import { useThrottledCallback } from '../hooks/use-throttled-callback'; + +export function useSyncStateThrottled( + state: RecoilValueReadOnly, + replicaState: RecoilState, + ms: number, +) { + const value = useRecoilValue(state); + const syncValue = useSetRecoilState(replicaState); + + const syncValueThrottled = useThrottledCallback(syncValue, ms); + + useEffect(() => { + syncValueThrottled(value); + }, [value, syncValueThrottled]); +} diff --git a/frontend/src/map/MapView.tsx b/frontend/src/map/MapView.tsx index bf0d0f59..1274be22 100644 --- a/frontend/src/map/MapView.tsx +++ b/frontend/src/map/MapView.tsx @@ -1,8 +1,15 @@ -import { Suspense, useCallback, useContext, useEffect, useLayoutEffect } from 'react'; -import { StaticMap } from 'react-map-gl'; -import { atom, useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil'; - +import { Suspense, useCallback, useEffect } from 'react'; +import { + atom, + useRecoilState, + useRecoilValue, + useResetRecoilState, + useSetRecoilState +} from 'recoil'; + +import { mapViewStateState, useSyncMapUrl } from '../state/map-view/map-view-state'; import { BoundingBox } from 'lib/bounding-box'; +import { BaseMap } from 'lib/data-map/BaseMap'; import { DataMap } from 'lib/data-map/DataMap'; import { DataMapTooltip } from 'lib/data-map/DataMapTooltip'; import { MapBoundsFitter } from 'lib/map/MapBoundsFitter'; @@ -12,7 +19,7 @@ import { MapHudAttributionControl, MapHudNavigationControl, MapHudScaleControl, -} from 'lib/map/hud/mapbox-controls'; +} from 'lib/map/hud/map-controls'; import { MapSearch } from 'lib/map/place-search/MapSearch'; import { PlaceSearchResult } from 'lib/map/place-search/use-place-search'; import { ErrorBoundary } from 'lib/react/ErrorBoundary'; @@ -26,13 +33,10 @@ import { globalStyleVariables } from '../theme'; import { useIsMobile } from '../use-is-mobile'; import { MapLayerSelection } from './layers/MapLayerSelection'; -import { backgroundState } from './layers/layers-state'; +import { backgroundState, showLabelsState } from './layers/layers-state'; import { MapLegend } from './legend/MapLegend'; import { TooltipContent } from './tooltip/TooltipContent'; -import { useBackgroundConfig } from './use-background-config'; -// import { ViewStateDebug } from 'lib/data-map/ViewStateDebug'; -import { zoomState } from 'state/zoom'; -import { ViewStateContext } from 'lib/data-map/DeckMap'; +import { useBasemapStyle } from './use-basemap-style'; export const mapFitBoundsState = atom({ key: 'mapFitBoundsState', @@ -116,9 +120,12 @@ const MapHudMobileLayout = () => { }; const MapViewContent = () => { + const [viewState, setViewState] = useRecoilState(mapViewStateState); const background = useRecoilValue(backgroundState); + const showLabels = useRecoilValue(showLabelsState); const viewLayers = useRecoilValue(viewLayersFlatState); const saveViewLayers = useSaveViewLayers(); + const { mapStyle, firstLabelId } = useBasemapStyle(background, showLabels); useEffect(() => { saveViewLayers(viewLayers); @@ -128,8 +135,6 @@ const MapViewContent = () => { const interactionGroups = useRecoilValue(interactionGroupsState); - const backgroundStyle = useBackgroundConfig(background); - const fitBounds = useRecoilValue(mapFitBoundsState); const resetFitBounds = useResetRecoilState(mapFitBoundsState); @@ -141,35 +146,26 @@ const MapViewContent = () => { const isMobile = useIsMobile(); return ( - - - + {isMobile ? : } - + ); }; -function ZoomSetter() { - const setZoom = useSetRecoilState(zoomState); - const { - viewState: { zoom }, - } = useContext(ViewStateContext); - - useLayoutEffect(() => { - setZoom(zoom); - }, [zoom, setZoom]); - - return null; -} export const MapView = () => ( diff --git a/frontend/src/map/layers/layers-state.ts b/frontend/src/map/layers/layers-state.ts index 883153e6..2c489491 100644 --- a/frontend/src/map/layers/layers-state.ts +++ b/frontend/src/map/layers/layers-state.ts @@ -1,6 +1,6 @@ import { atom } from 'recoil'; -import { BackgroundName } from '../../config/backgrounds'; +import { BackgroundName } from '../../config/basemaps'; export const backgroundState = atom({ key: 'background', diff --git a/frontend/src/map/use-background-config.ts b/frontend/src/map/use-background-config.ts deleted file mode 100644 index 2b2e19f0..00000000 --- a/frontend/src/map/use-background-config.ts +++ /dev/null @@ -1,23 +0,0 @@ -import mapValues from 'lodash/mapValues'; -import merge from 'lodash/merge'; -import { useMemo } from 'react'; - -import { BACKGROUNDS, BackgroundName } from 'config/backgrounds'; - -function visible(isVisible: boolean): 'visible' | 'none' { - return isVisible ? 'visible' : 'none'; -} - -function makeBackgroundConfig(background: BackgroundName) { - return { - version: 8, - sources: mapValues(BACKGROUNDS, (b) => b.source), - layers: Object.values(BACKGROUNDS).map((b) => - merge(b.layer, { layout: { visibility: visible(background === b.id) } }), - ), - }; -} - -export function useBackgroundConfig(background: BackgroundName) { - return useMemo(() => makeBackgroundConfig(background), [background]); -} diff --git a/frontend/src/map/use-basemap-style.ts b/frontend/src/map/use-basemap-style.ts new file mode 100644 index 00000000..5db1edaa --- /dev/null +++ b/frontend/src/map/use-basemap-style.ts @@ -0,0 +1,73 @@ +import cloneDeep from 'lodash/cloneDeep'; +import set from 'lodash/set'; +import { StyleSpecification } from 'maplibre-gl'; +import { useEffect, useMemo } from 'react'; +import { useFetch } from 'use-http'; + +import { + BACKGROUNDS, + BACKGROUND_ATTRIBUTIONS, + BASEMAP_STYLE_URL, + BackgroundName, + BackgroundSpecification, + LABELS_LAYERS, +} from '../config/basemaps'; + +function visible(isVisible: boolean): 'visible' | 'none' { + return isVisible ? 'visible' : 'none'; +} + +function makeBasemapStyle( + baseStyle: StyleSpecification, + backgroundConfig: BackgroundSpecification, + showLabels: boolean, +): StyleSpecification { + const backgroundLayersLookup = new Set(backgroundConfig.layers); + const labelLayersLookup = new Set(LABELS_LAYERS); + + const style = cloneDeep(baseStyle); + + for (const layer of style.layers) { + const { id } = layer; + + const isVisible = backgroundLayersLookup.has(id) || (showLabels && labelLayersLookup.has(id)); + set(layer, 'layout.visibility', visible(isVisible)); + } + + return style; +} + +export function useBasemapStyle( + background: BackgroundName, + showLabels: boolean, +): { mapStyle: StyleSpecification; firstLabelId: string | undefined } { + const backgroundConfig = BACKGROUNDS[background]; + const { + get, + data: baseStyle = { + version: 8, + sources: {}, + layers: [], + }, + } = useFetch(BASEMAP_STYLE_URL, { suspense: true }); + + useEffect(() => { + get(); + }, [get]); + + const mapStyle = useMemo( + () => makeBasemapStyle(baseStyle, backgroundConfig, showLabels), + [baseStyle, backgroundConfig, showLabels], + ); + + const firstLabelId = showLabels ? LABELS_LAYERS[0] : undefined; + + return { + mapStyle, + firstLabelId, + }; +} + +export function useBackgroundAttribution(background: BackgroundName) { + return BACKGROUND_ATTRIBUTIONS[background]; +} diff --git a/frontend/src/state/layers/view-layers.ts b/frontend/src/state/layers/view-layers.ts index 7c32a46a..ce127200 100644 --- a/frontend/src/state/layers/view-layers.ts +++ b/frontend/src/state/layers/view-layers.ts @@ -1,11 +1,7 @@ -import { regionLabelsDeckLayer } from 'config/regions/region-labels-deck-layer'; import { regionBoundariesViewLayer } from 'config/regions/boundaries-view-layer'; import { ViewLayer, viewOnlyLayer } from 'lib/data-map/view-layers'; -import { backgroundState, showLabelsState } from 'map/layers/layers-state'; import { selector } from 'recoil'; import { truthyKeys } from 'lib/helpers'; -import { labelsLayer } from 'config/deck-layers/labels-layer'; -import { isRetinaState } from 'state/is-retina'; import { ConfigTree } from 'lib/nested-config/config-tree'; import { populationViewLayer } from 'config/regions/population-view-layer'; @@ -53,9 +49,6 @@ export const viewLayersState = selector>({ get: ({ get }) => { const showRegions = get(sectionVisibilityState('regions')); const regionLevel = get(regionLevelState); - const background = get(backgroundState); - const showLabels = get(showLabelsState); - const isRetina = get(isRetinaState); return [ // administrative region boundaries or population density @@ -79,22 +72,7 @@ export const viewLayersState = selector>({ get(droughtOptionsLayerState), - get(featureBoundingBoxLayerState), - - showLabels && [ - // basemap labels - viewOnlyLayer('labels', () => labelsLayer(isRetina)), - - // administrative regions labels - showRegions && - viewOnlyLayer(`boundaries_${regionLevel}-text`, () => - regionLabelsDeckLayer(regionLevel, background), - ), - ], - - /** - * CAUTION: for some reason, vector layers put here are obscured by the 'labels' semi-transparent raster layer - */ + get(featureBoundingBoxLayerState) ]; }, }); diff --git a/frontend/src/state/map-view/map-url.ts b/frontend/src/state/map-view/map-url.ts new file mode 100644 index 00000000..165842aa --- /dev/null +++ b/frontend/src/state/map-view/map-url.ts @@ -0,0 +1,69 @@ +import { number } from '@recoiljs/refine'; +import { DefaultValue, atom } from 'recoil'; +import { WriteAtom, urlSyncEffect } from 'recoil-sync'; + +import { mapViewConfig } from '../../config/map-view'; + +/** + * Makes a recoil-sync write function that saves a number with up to `maximumFractionDigits` + */ +function makeWriteNumber(itemKey: string, maximumFractionDigits: number) { + const writeNumber: WriteAtom = ({ write, reset }, x) => { + if (x instanceof DefaultValue) { + reset(itemKey); + } else { + write( + itemKey, + +x.toLocaleString(undefined, { + minimumFractionDigits: 1, + maximumFractionDigits, + useGrouping: false, + }), + ); + } + }; + + return writeNumber; +} + +export const mapZoomUrlState = atom({ + key: 'mapZoomUrl', + default: mapViewConfig.initialViewState.zoom, + effects: [ + urlSyncEffect({ + storeKey: 'url-json', + itemKey: 'z', + refine: number(), + write: makeWriteNumber('z', 2), + syncDefault: true, + }), + ], +}); + +export const mapLonUrlState = atom({ + key: 'mapLonUrl', + default: mapViewConfig.initialViewState.longitude, + effects: [ + urlSyncEffect({ + storeKey: 'url-json', + itemKey: 'x', + refine: number(), + write: makeWriteNumber('x', 5), + syncDefault: true, + }), + ], +}); + +export const mapLatUrlState = atom({ + key: 'mapLatUrl', + default: mapViewConfig.initialViewState.latitude, + effects: [ + urlSyncEffect({ + storeKey: 'url-json', + itemKey: 'y', + refine: number(), + write: makeWriteNumber('y', 5), + syncDefault: true, + }), + ], +}); diff --git a/frontend/src/state/map-view/map-view-state.ts b/frontend/src/state/map-view/map-view-state.ts new file mode 100644 index 00000000..fd478fb9 --- /dev/null +++ b/frontend/src/state/map-view/map-view-state.ts @@ -0,0 +1,58 @@ +import omit from 'lodash/omit'; +import { DefaultValue, atom, selector } from 'recoil'; + +import { useSyncStateThrottled } from '../../lib/recoil/sync-state-throttled'; + +import { mapViewConfig } from '../../config/map-view'; + +import { mapLatUrlState, mapLonUrlState, mapZoomUrlState } from './map-url'; + +const mapLatState = atom({ key: 'mapLat', default: mapLatUrlState }); +const mapLonState = atom({ key: 'mapLon', default: mapLonUrlState }); +const mapZoomState = atom({ key: 'mapZoom', default: mapZoomUrlState }); + +const INITIAL_VIEW_STATE = { + ...mapViewConfig.initialViewState, + ...mapViewConfig.viewLimits, +}; + +const INITIAL_NON_COORDS_STATE = omit(INITIAL_VIEW_STATE, ['latitude', 'longitude', 'zoom']); + +export const nonCoordsMapViewStateState = atom({ + key: 'nonCoordsMapViewState', + default: { ...INITIAL_NON_COORDS_STATE }, +}); + +export const mapViewStateState = selector({ + key: 'mapViewState', + dangerouslyAllowMutability: true, + get: ({ get }) => { + const viewState = { + ...get(nonCoordsMapViewStateState), + latitude: get(mapLatState), + longitude: get(mapLonState), + zoom: get(mapZoomState), + }; + return viewState; + }, + set: ({ set, reset }, newValue) => { + if (newValue instanceof DefaultValue) { + reset(mapZoomState); + reset(mapLatState); + reset(mapLonState); + reset(nonCoordsMapViewStateState); + } else { + const { latitude, longitude, zoom, ...nonCoords } = newValue; + set(mapZoomState, zoom); + set(mapLonState, longitude); + set(mapLatState, latitude); + set(nonCoordsMapViewStateState, nonCoords); + } + }, +}); + +export function useSyncMapUrl() { + useSyncStateThrottled(mapLatState, mapLatUrlState, 2000); + useSyncStateThrottled(mapLonState, mapLonUrlState, 2000); + useSyncStateThrottled(mapZoomState, mapZoomUrlState, 2000); +} From f6fb2f0df0e1509a0b6a549c46bf4532a0045b24 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 30 May 2024 09:48:59 +0100 Subject: [PATCH 2/5] clean up legends --- frontend/src/config/backgrounds.ts | 44 ---------------------- frontend/src/map/legend/GradientLegend.tsx | 23 +++++++++-- frontend/src/map/legend/RasterLegend.tsx | 8 ++++ 3 files changed, 27 insertions(+), 48 deletions(-) delete mode 100644 frontend/src/config/backgrounds.ts diff --git a/frontend/src/config/backgrounds.ts b/frontend/src/config/backgrounds.ts deleted file mode 100644 index 1785a7a4..00000000 --- a/frontend/src/config/backgrounds.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { makeConfig } from 'lib/helpers'; - -export const BACKGROUNDS = makeConfig([ - { - id: 'light', - label: 'Map', - source: { - id: 'light', - type: 'raster', - tiles: [ - 'https://tiles.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://a.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://b.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://c.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - 'https://d.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', - ], - tileSize: 256, - }, - layer: { - id: 'bg-light', - source: 'light', - type: 'raster', - }, - }, - { - id: 'satellite', - label: 'Satellite', - source: { - id: 'satellite', - type: 'raster', - tiles: [ - 'https://tiles.maps.eox.at/wmts/1.0.0/s2cloudless-2020_3857/default/GoogleMapsCompatible/{z}/{y}/{x}.png', - ], - tileSize: 256, - }, - layer: { - id: 'bg-satellite', - source: 'satellite', - type: 'raster', - }, - }, -]); - -export type BackgroundName = keyof typeof BACKGROUNDS; diff --git a/frontend/src/map/legend/GradientLegend.tsx b/frontend/src/map/legend/GradientLegend.tsx index 28738428..8dd21288 100644 --- a/frontend/src/map/legend/GradientLegend.tsx +++ b/frontend/src/map/legend/GradientLegend.tsx @@ -1,10 +1,12 @@ import { Box, Typography } from '@mui/material'; -import { FC } from 'react'; +import { FC, ReactNode, memo } from 'react'; + +import { ColorValue } from './RasterLegend'; const legendHeight = 10; const LegendGradient: FC<{ - colorMapValues: any[]; + colorMapValues: ColorValue[]; getValueLabel: (value: number) => string; }> = ({ colorMapValues, getValueLabel }) => { return ( @@ -16,7 +18,20 @@ const LegendGradient: FC<{ ); }; -export const GradientLegend = ({ label, range, colorMapValues, getValueLabel }) => ( +export interface GradientLegendProps { + label: string | ReactNode; + description?: string; + range: [number, number]; + colorMapValues: ColorValue[]; + getValueLabel: (value: number) => string; +} + +export const GradientLegend:FC = memo(({ + label, + range, + colorMapValues, + getValueLabel +}) => ( {label} -); +)); diff --git a/frontend/src/map/legend/RasterLegend.tsx b/frontend/src/map/legend/RasterLegend.tsx index b924bb55..9c89553d 100644 --- a/frontend/src/map/legend/RasterLegend.tsx +++ b/frontend/src/map/legend/RasterLegend.tsx @@ -4,6 +4,14 @@ import { ViewLayer } from 'lib/data-map/view-layers'; import { FC, useCallback } from 'react'; import { GradientLegend } from './GradientLegend'; import { useRasterColorMapValues } from './use-color-map-values'; +export interface ColorValue { + color: string; + value: any; +} +export interface RasterColorMapValues { + colorMapValues: ColorValue[]; + rangeTruncated: [boolean, boolean]; +} export const RasterLegend: FC<{ viewLayer: ViewLayer }> = ({ viewLayer }) => { const { From 3e768fadedd7e2be15511df584ee4c44aa3e520d Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 30 May 2024 09:53:48 +0100 Subject: [PATCH 3/5] fix broken import --- frontend/src/config/regions/region-labels-deck-layer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/config/regions/region-labels-deck-layer.ts b/frontend/src/config/regions/region-labels-deck-layer.ts index 94b15fb8..5d46006f 100644 --- a/frontend/src/config/regions/region-labels-deck-layer.ts +++ b/frontend/src/config/regions/region-labels-deck-layer.ts @@ -1,6 +1,6 @@ import { mvtLayer } from 'lib/deck/layers/base'; -import { BackgroundName } from 'config/backgrounds'; +import { BackgroundName } from 'config/basemaps'; import { RegionLevel, REGIONS_METADATA } from './metadata'; const LIGHT_TEXT = [240, 240, 240, 255]; From 19dcc50b713157b828f4ff3c46af56f8ea7b8c7c Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 30 May 2024 10:01:52 +0100 Subject: [PATCH 4/5] fix type error --- frontend/src/lib/data-map/DataMap.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/lib/data-map/DataMap.tsx b/frontend/src/lib/data-map/DataMap.tsx index ccaed85e..6ca04f85 100644 --- a/frontend/src/lib/data-map/DataMap.tsx +++ b/frontend/src/lib/data-map/DataMap.tsx @@ -8,6 +8,7 @@ import { useDataLoadTrigger } from './use-data-load-trigger'; import { DeckGLOverlay } from '../map/DeckGLOverlay'; import { useInteractions } from './interactions/use-interactions'; import { ViewLayer, ViewLayerParams } from './view-layers'; +import { LayersList } from 'deck.gl/typed'; export interface DataMapProps { beforeId: string, @@ -46,7 +47,7 @@ export const DataMap: FC = ({ ({ zoom }: { zoom: number }) => viewLayers.map((viewLayer) => makeDeckLayers(viewLayer, viewLayersParams[viewLayer.id], zoom, beforeId), - ), + ) as LayersList, [beforeId, viewLayers, viewLayersParams], ); From 7faec71147bc7381c2dad9302f118336b9628c43 Mon Sep 17 00:00:00 2001 From: Jim O'Donnell Date: Thu, 30 May 2024 10:17:17 +0100 Subject: [PATCH 5/5] Format with prettier --- frontend/src/lib/data-map/DataMap.tsx | 3 +- frontend/src/lib/map/MapBoundsFitter.tsx | 2 +- frontend/src/map/MapView.tsx | 8 +-- frontend/src/map/legend/GradientLegend.tsx | 63 +++++++++++----------- frontend/src/state/layers/view-layers.ts | 2 +- 5 files changed, 35 insertions(+), 43 deletions(-) diff --git a/frontend/src/lib/data-map/DataMap.tsx b/frontend/src/lib/data-map/DataMap.tsx index 6ca04f85..b4420236 100644 --- a/frontend/src/lib/data-map/DataMap.tsx +++ b/frontend/src/lib/data-map/DataMap.tsx @@ -11,7 +11,7 @@ import { ViewLayer, ViewLayerParams } from './view-layers'; import { LayersList } from 'deck.gl/typed'; export interface DataMapProps { - beforeId: string, + beforeId: string; viewLayers: ViewLayer[]; viewLayersParams: Record; interactionGroups: any; @@ -42,7 +42,6 @@ export const DataMap: FC = ({ const dataLoadTrigger = useDataLoadTrigger(dataLoaders); - const layersFunction = useCallback( ({ zoom }: { zoom: number }) => viewLayers.map((viewLayer) => diff --git a/frontend/src/lib/map/MapBoundsFitter.tsx b/frontend/src/lib/map/MapBoundsFitter.tsx index 25b08827..def3d017 100644 --- a/frontend/src/lib/map/MapBoundsFitter.tsx +++ b/frontend/src/lib/map/MapBoundsFitter.tsx @@ -30,4 +30,4 @@ export function getBoundingBoxViewState( const { latitude, longitude, zoom } = viewport.fitBounds(deckBbox, { padding: 20 }); return { latitude, longitude, zoom }; -} \ No newline at end of file +} diff --git a/frontend/src/map/MapView.tsx b/frontend/src/map/MapView.tsx index 1274be22..40389dbb 100644 --- a/frontend/src/map/MapView.tsx +++ b/frontend/src/map/MapView.tsx @@ -4,7 +4,7 @@ import { useRecoilState, useRecoilValue, useResetRecoilState, - useSetRecoilState + useSetRecoilState, } from 'recoil'; import { mapViewStateState, useSyncMapUrl } from '../state/map-view/map-view-state'; @@ -146,11 +146,7 @@ const MapViewContent = () => { const isMobile = useIsMobile(); return ( - + string; } -export const GradientLegend:FC = memo(({ - label, - range, - colorMapValues, - getValueLabel -}) => ( - - {label} - - {colorMapValues && ( - - )} +export const GradientLegend: FC = memo( + ({ label, range, colorMapValues, getValueLabel }) => ( + + {label} + + {colorMapValues && ( + + )} + + + {colorMapValues && ( + <> + + {getValueLabel(range[0])} + + + {getValueLabel(range[1])} + + + )} + - - {colorMapValues && ( - <> - - {getValueLabel(range[0])} - - - {getValueLabel(range[1])} - - - )} - - -)); + ), +); diff --git a/frontend/src/state/layers/view-layers.ts b/frontend/src/state/layers/view-layers.ts index ce127200..fcdc8f26 100644 --- a/frontend/src/state/layers/view-layers.ts +++ b/frontend/src/state/layers/view-layers.ts @@ -72,7 +72,7 @@ export const viewLayersState = selector>({ get(droughtOptionsLayerState), - get(featureBoundingBoxLayerState) + get(featureBoundingBoxLayerState), ]; }, });