From 07eef69d340819a6066d41153a590d49e292c388 Mon Sep 17 00:00:00 2001 From: Mathias Boeck Date: Wed, 29 Nov 2023 16:36:37 +0100 Subject: [PATCH] Maplibre (#203) * fix: update package-lock for version 12.0.0-alpha.2 * feat: export new type TFiltertypesUncap * docs: description for create libraries and add examples to demo-maps * feat: add place-label style for internal testing * BREAKING CHANGE: remove exported Tgroupfiltertype from map-ol and map-cesium - use from services-layers * docs: add change as a commit label for changes that only could introduce braking changes * fix: budgets for demo-maps * feat: New UKIS library export utilities for other libraries * fix: Merge conflict * feat: create angular library for maplibre * feat: add and extend @dlr-eoc/map-maplibre from internal repo * test: add tests for maplibre service * test: add tests for maplibre component * test: add tests for maplibre layers helpers[201~ * feat: add demo for maplibre * fix: use min/max zoom from ukis layer * fix: move layers and layergroups * refactor: use const for metadata * refactor: export Specification from base helpers * refactor: rename groupId to ukisLayerId * refactor: export get opacity from base helpers * refactor: use createLayer from layer helpers * refactor: reuse getAllLayers * change: remove not used function * fix: remove source only if not used by another layer * refactor: change order of layers function * refactor: use function to change order of layers * test: add tests for maplibre helpers * fix: add devDependencies for test and build * refactor: remove not used import * test: add shared-assets to karma config * refactor: rename import * fix: use subdomains in wms * feat: use bbox as bounds for vector and raster source and clusteroptions * feat: update layer parameters and sources * build: add deps for lib build * refactor: test aws open terrain-tiles * refactor: test update geojson layer * feat: change style of map controls like map-ol * refactor: add test wms layer to switch styles * refactor: load styles for cesium only on there route * refactor: remove max width in demo maps cards * test: fix destroy viewer after test - Too many active WebGL contexts, and include css * docs: hint for todo - replace function * refactor: remove not used function * fix: check for source before check diff * refactor: generate tiles array in test * fix: changed customLayer import. --------- Co-authored-by: ange_lu --- CHANGELOG.md | 4 + package-lock.json | 378 ++++++++- package.json | 3 +- projects/demo-maps/package.json | 3 +- .../demo-maps/src/app/app-routing.module.ts | 9 + .../route-example-maplibre.component.html | 42 + .../route-example-maplibre.component.scss | 1 + .../route-example-maplibre.component.spec.ts | 23 + .../route-example-maplibre.component.ts | 717 ++++++++++++++++++ .../route-example-maplibre.module.ts | 32 + .../demo-maps/src/assets/route-maplibre.jpg | Bin 0 -> 44940 bytes projects/map-maplibre/README.md | 138 ++++ projects/map-maplibre/karma.conf.js | 53 ++ projects/map-maplibre/ng-package.json | 15 + projects/map-maplibre/package.json | 33 + .../src/lib/map-maplibre.component.html | 1 + .../src/lib/map-maplibre.component.scss | 69 ++ .../src/lib/map-maplibre.component.spec.ts | 87 +++ .../src/lib/map-maplibre.component.ts | 401 ++++++++++ .../src/lib/map-maplibre.module.ts | 18 + .../src/lib/map-maplibre.service.spec.ts | 294 +++++++ .../src/lib/map-maplibre.service.ts | 234 ++++++ .../src/lib/maplibre-layers.helpers.spec.ts | 317 ++++++++ .../src/lib/maplibre-layers.helpers.ts | 602 +++++++++++++++ .../src/lib/maplibre.helpers.spec.ts | 522 +++++++++++++ .../map-maplibre/src/lib/maplibre.helpers.ts | 368 +++++++++ projects/map-maplibre/src/public-api.ts | 9 + projects/map-maplibre/src/test.ts | 27 + projects/map-maplibre/tsconfig.lib.json | 15 + projects/map-maplibre/tsconfig.lib.prod.json | 10 + projects/map-maplibre/tsconfig.spec.json | 17 + projects/map-ol/src/lib/map-ol.component.ts | 1 + 32 files changed, 4417 insertions(+), 26 deletions(-) create mode 100644 projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.html create mode 100644 projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.scss create mode 100644 projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.spec.ts create mode 100644 projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.ts create mode 100644 projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.module.ts create mode 100644 projects/demo-maps/src/assets/route-maplibre.jpg create mode 100644 projects/map-maplibre/README.md create mode 100644 projects/map-maplibre/karma.conf.js create mode 100644 projects/map-maplibre/ng-package.json create mode 100644 projects/map-maplibre/package.json create mode 100644 projects/map-maplibre/src/lib/map-maplibre.component.html create mode 100644 projects/map-maplibre/src/lib/map-maplibre.component.scss create mode 100644 projects/map-maplibre/src/lib/map-maplibre.component.spec.ts create mode 100644 projects/map-maplibre/src/lib/map-maplibre.component.ts create mode 100644 projects/map-maplibre/src/lib/map-maplibre.module.ts create mode 100644 projects/map-maplibre/src/lib/map-maplibre.service.spec.ts create mode 100644 projects/map-maplibre/src/lib/map-maplibre.service.ts create mode 100644 projects/map-maplibre/src/lib/maplibre-layers.helpers.spec.ts create mode 100644 projects/map-maplibre/src/lib/maplibre-layers.helpers.ts create mode 100644 projects/map-maplibre/src/lib/maplibre.helpers.spec.ts create mode 100644 projects/map-maplibre/src/lib/maplibre.helpers.ts create mode 100644 projects/map-maplibre/src/public-api.ts create mode 100644 projects/map-maplibre/src/test.ts create mode 100644 projects/map-maplibre/tsconfig.lib.json create mode 100644 projects/map-maplibre/tsconfig.lib.prod.json create mode 100644 projects/map-maplibre/tsconfig.spec.json diff --git a/CHANGELOG.md b/CHANGELOG.md index eca43dbae..65bf1201b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,10 @@ * **@dlr-eoc/utilities:** - New UKIS library export utilities for other libraries. +* **@dlr-eoc/map-maplibre:** + - A example has been added to the demo-maps to show how to work with the new maplibre library. + - New UKIS library for working with [maplibre](https://maplibre.org/) was added. + * **@dlr-eoc/map-cesium:** - New UKIS library for working with [CesiumJS](https://github.com/CesiumGS/cesium) was added. diff --git a/package-lock.json b/package-lock.json index 08ce18258..ba37057a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,7 +29,8 @@ "projects/map-three", "projects/shared-assets", "projects/map-cesium", - "projects/utilities" + "projects/utilities", + "projects/map-maplibre" ], "dependencies": { "@angular-devkit/core": "^14.2.11", @@ -3331,6 +3332,10 @@ "resolved": "projects/map-cesium", "link": true }, + "node_modules/@dlr-eoc/map-maplibre": { + "resolved": "projects/map-maplibre", + "link": true + }, "node_modules/@dlr-eoc/map-ol": { "resolved": "projects/map-ol", "link": true @@ -3688,6 +3693,18 @@ "@lit-labs/ssr-dom-shim": "^1.0.0" } }, + "node_modules/@mapbox/geojson-rewind": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", + "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", + "dependencies": { + "get-stream": "^6.0.1", + "minimist": "^1.2.6" + }, + "bin": { + "geojson-rewind": "geojson-rewind" + } + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -3722,11 +3739,155 @@ "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==" }, + "node_modules/@mapbox/tiny-sdf": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", + "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==" + }, + "node_modules/@mapbox/togeojson": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@mapbox/togeojson/-/togeojson-0.16.0.tgz", + "integrity": "sha512-PeBrRQ+kuVP5j3lqa5JtnYBd9E7eQdWnsmOmUq8aWs0caNzLbCqnXSkKxrIGURukf7lZ82aOxjustLRX3f9GOA==", + "dependencies": { + "concat-stream": "~1.5.1", + "minimist": "1.2.0", + "xmldom": "~0.1.19" + }, + "bin": { + "togeojson": "togeojson" + } + }, + "node_modules/@mapbox/togeojson/node_modules/concat-stream": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz", + "integrity": "sha512-H6xsIBfQ94aESBG8jGHXQ7i5AEpy5ZeVaLDOisDICiTCKpqEfr34/KmTrspKQNoLKNu9gTkovlpQcUi630AKiQ==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "~2.0.0", + "typedarray": "~0.0.5" + } + }, + "node_modules/@mapbox/togeojson/node_modules/minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==" + }, + "node_modules/@mapbox/togeojson/node_modules/process-nextick-args": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", + "integrity": "sha512-yN0WQmuCX63LP/TMvAg31nvT6m4vDqJEiiv2CAZqWOGNWutc9DfDk1NPYYmKUFmaVM2UwDowH4u5AHWYP/jxKw==" + }, + "node_modules/@mapbox/togeojson/node_modules/readable-stream": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz", + "integrity": "sha512-TXcFfb63BQe1+ySzsHZI/5v1aJPCShfqvWJ64ayNImXMsN1Cd0YGk/wm8KB7/OeessgPc9QvS9Zou8QTkFzsLw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "~1.0.0", + "process-nextick-args": "~1.0.6", + "string_decoder": "~0.10.x", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/@mapbox/togeojson/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==" + }, + "node_modules/@mapbox/togeojson/node_modules/xmldom": { + "version": "0.1.31", + "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.31.tgz", + "integrity": "sha512-yS2uJflVQs6n+CyjHoaBmVSqIDevTAWrzMmjG1Gc7h1qQ7uVozNhEPJAwZXWyGQ/Gafo3fCwrcaokezLPupVyQ==", + "deprecated": "Deprecated due to CVE-2021-21366 resolved in 0.5.0", + "engines": { + "node": ">=0.1" + } + }, "node_modules/@mapbox/unitbezier": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==" }, + "node_modules/@mapbox/vector-tile": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", + "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", + "dependencies": { + "@mapbox/point-geometry": "~0.1.0" + } + }, + "node_modules/@mapbox/whoots-js": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", + "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-19.3.0.tgz", + "integrity": "sha512-ZbhX9CTV+Z7vHwkRIasDOwTSzr76e8Q6a55RMsAibjyX6+P0ZNL1qAKNzOjjBDP3+aEfNMl7hHo5knuY6pTAUQ==", + "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/@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/@maplibre/maplibre-gl-style-spec/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/@maplibre/maplibre-gl-style-spec/node_modules/sort-asc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sort-asc/-/sort-asc-0.2.0.tgz", + "integrity": "sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/sort-desc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/sort-desc/-/sort-desc-0.2.0.tgz", + "integrity": "sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@maplibre/maplibre-gl-style-spec/node_modules/sort-object": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sort-object/-/sort-object-3.0.3.tgz", + "integrity": "sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==", + "dependencies": { + "bytewise": "^1.1.0", + "get-value": "^2.0.2", + "is-extendable": "^0.1.1", + "sort-asc": "^0.2.0", + "sort-desc": "^0.2.0", + "union-value": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/@michaellangbein/jsonix": { "version": "3.0.1-SNAPSHOT-3", "resolved": "https://registry.npmjs.org/@michaellangbein/jsonix/-/jsonix-3.0.1-SNAPSHOT-3.tgz", @@ -4232,8 +4393,7 @@ "node_modules/@types/geojson": { "version": "7946.0.10", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.10.tgz", - "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==", - "dev": true + "integrity": "sha512-Nmh0K3iWQJzniTuPRcJn5hxXkfB1T1pgB89SBig5PlJQU5yocazeu4jATJlaA0GYFKWMqDdvYemoSnF2pXgLVA==" }, "node_modules/@types/http-proxy": { "version": "1.17.11", @@ -4265,6 +4425,21 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/mapbox__point-geometry": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", + "integrity": "sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA==" + }, + "node_modules/@types/mapbox__vector-tile": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.0.tgz", + "integrity": "sha512-kDwVreQO5V4c8yAxzZVQLE5tyWF+IPToAanloQaSnwfXmIcJ7cyOrv8z4Ft4y7PsLYmhWXmON8MBV8RX0Rgr8g==", + "dependencies": { + "@types/geojson": "*", + "@types/mapbox__point-geometry": "*", + "@types/pbf": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", @@ -4292,6 +4467,11 @@ "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" }, + "node_modules/@types/pbf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.2.tgz", + "integrity": "sha512-EDrLIPaPXOZqDjrkzxxbX7UlJSeQVgah3i0aA4pOSzmK9zq3BIh7/MZIQxED7slJByvKM4Gc6Hypyu2lJzh3SQ==" + }, "node_modules/@types/q": { "version": "0.0.32", "resolved": "https://registry.npmjs.org/@types/q/-/q-0.0.32.tgz", @@ -4369,6 +4549,14 @@ "@types/node": "*" } }, + "node_modules/@types/supercluster": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.0.tgz", + "integrity": "sha512-6JapQ2GmEkH66r23BK49I+u6zczVDGTtiJEVvKDYZVSm/vepWaJuTq6BXzJ6I4agG5s8vA1KM7m/gXWDg03O4Q==", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/toposort": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/toposort/-/toposort-2.0.3.tgz", @@ -5004,7 +5192,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5097,7 +5284,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -5811,6 +5997,23 @@ "node": ">= 0.8" } }, + "node_modules/bytewise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bytewise/-/bytewise-1.1.0.tgz", + "integrity": "sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==", + "dependencies": { + "bytewise-core": "^1.2.2", + "typewise": "^1.0.3" + } + }, + "node_modules/bytewise-core": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bytewise-core/-/bytewise-core-1.2.3.tgz", + "integrity": "sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==", + "dependencies": { + "typewise-core": "^1.2" + } + }, "node_modules/cacache": { "version": "16.1.2", "resolved": "https://registry.npmjs.org/cacache/-/cacache-16.1.2.tgz", @@ -6669,8 +6872,7 @@ "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" }, "node_modules/cors": { "version": "2.8.5", @@ -9460,7 +9662,6 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, "dependencies": { "assign-symbols": "^1.0.0", "is-extendable": "^1.0.1" @@ -9473,7 +9674,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, "dependencies": { "is-plain-object": "^2.0.4" }, @@ -9952,6 +10152,11 @@ "node": ">=6.9.0" } }, + "node_modules/geojson-vt": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-3.2.1.tgz", + "integrity": "sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==" + }, "node_modules/geotiff": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz", @@ -10011,7 +10216,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "engines": { "node": ">=10" }, @@ -10023,7 +10227,6 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -10037,6 +10240,11 @@ "assert-plus": "^1.0.0" } }, + "node_modules/gl-matrix": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", + "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==" + }, "node_modules/glob": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/glob/-/glob-8.0.3.tgz", @@ -10127,6 +10335,24 @@ "node": ">= 0.10" } }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -11185,7 +11411,6 @@ "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -11324,7 +11549,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, "dependencies": { "isobject": "^3.0.1" }, @@ -11443,8 +11667,7 @@ "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" }, "node_modules/isbinaryfile": { "version": "4.0.10", @@ -11467,7 +11690,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12147,7 +12369,6 @@ "version": "6.0.3", "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -12649,6 +12870,50 @@ "resolved": "https://registry.npmjs.org/mapbox-to-css-font/-/mapbox-to-css-font-2.4.2.tgz", "integrity": "sha512-f+NBjJJY4T3dHtlEz1wCG7YFlkODEjFIYlxDdLIDMNpkSksqTt+l/d4rjuwItxuzkuMFvPyrjzV2lxRM4ePcIA==" }, + "node_modules/maplibre-gl": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-3.3.0.tgz", + "integrity": "sha512-LDia3b8u2S8qtl50n8TYJM0IPLzfc01KDc71LNuydvDiEXAGBI5togty+juVtUipRZZjs4dAW6xhgrabc6lIgw==", + "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": "^19.3.0", + "@types/geojson": "^7946.0.10", + "@types/mapbox__point-geometry": "^0.1.2", + "@types/mapbox__vector-tile": "^1.3.0", + "@types/pbf": "^3.0.2", + "@types/supercluster": "^7.1.0", + "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": ">=16.14.0", + "npm": ">=8.1.0" + }, + "funding": { + "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" + } + }, + "node_modules/maplibre-gl/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/marked": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -13115,6 +13380,11 @@ "node": "*" } }, + "node_modules/murmurhash-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", + "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==" + }, "node_modules/mute-stream": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", @@ -15316,6 +15586,11 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "dev": true }, + "node_modules/potpack": { + "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/prelude-ls": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", @@ -17076,7 +17351,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, "dependencies": { "extend-shallow": "^2.0.1", "is-extendable": "^0.1.1", @@ -17091,7 +17365,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, "dependencies": { "is-extendable": "^0.1.0" }, @@ -17671,7 +17944,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, "dependencies": { "extend-shallow": "^3.0.0" }, @@ -18233,6 +18505,14 @@ "minimist": "^1.1.0" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -18574,6 +18854,11 @@ "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", "dev": true }, + "node_modules/tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -18947,8 +19232,7 @@ "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { "version": "4.8.4", @@ -18962,6 +19246,19 @@ "node": ">=4.2.0" } }, + "node_modules/typewise": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typewise/-/typewise-1.0.3.tgz", + "integrity": "sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==", + "dependencies": { + "typewise-core": "^1.2.0" + } + }, + "node_modules/typewise-core": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", + "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" + }, "node_modules/ua-parser-js": { "version": "0.7.35", "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", @@ -19068,7 +19365,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", @@ -19338,6 +19634,16 @@ "node": ">=0.10.0" } }, + "node_modules/vt-pbf": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", + "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", + "dependencies": { + "@mapbox/point-geometry": "0.1.0", + "@mapbox/vector-tile": "^1.3.1", + "pbf": "^3.2.1" + } + }, "node_modules/w3c-schemas": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/w3c-schemas/-/w3c-schemas-1.4.0.tgz", @@ -19857,7 +20163,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -20353,6 +20658,7 @@ "@dlr-eoc/base-layers-raster": "12.0.0-alpha.2", "@dlr-eoc/layer-control": "12.0.0-alpha.2", "@dlr-eoc/map-cesium": "12.0.0-alpha.2", + "@dlr-eoc/map-maplibre": "12.0.0-alpha.2", "@dlr-eoc/map-ol": "12.0.0-alpha.2", "@dlr-eoc/map-three": "12.0.0-alpha.2", "@dlr-eoc/map-tools": "12.0.0-alpha.2", @@ -20430,6 +20736,29 @@ "rxjs": "^6.6.7" } }, + "projects/map-maplibre": { + "name": "@dlr-eoc/map-maplibre", + "version": "12.0.0-alpha.2", + "license": "Apache-2.0", + "dependencies": { + "@dlr-eoc/services-layers": "12.0.0-alpha.2", + "@dlr-eoc/services-map-state": "12.0.0-alpha.2", + "@dlr-eoc/utilities": "12.0.0-alpha.2", + "@mapbox/togeojson": "0.16.0", + "maplibre-gl": "^3.3.0", + "tslib": "^2.3.0" + }, + "devDependencies": { + "@dlr-eoc/base-layers-raster": "12.0.0-alpha.2", + "@dlr-eoc/shared-assets": "12.0.0-alpha.2", + "ol": "^7.3.0" + }, + "peerDependencies": { + "@angular/common": "^14.2.0", + "@angular/core": "^14.2.0", + "rxjs": "^6.6.7" + } + }, "projects/map-ol": { "name": "@dlr-eoc/map-ol", "version": "12.0.0-alpha.2", @@ -20651,6 +20980,7 @@ } }, "projects/utilities": { + "name": "@dlr-eoc/utilities", "version": "12.0.0-alpha.2", "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 4f27185bc..f49632418 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,8 @@ "projects/map-three", "projects/shared-assets", "projects/map-cesium", - "projects/utilities" + "projects/utilities", + "projects/map-maplibre" ], "engines": { "node": ">= 18.13.0", diff --git a/projects/demo-maps/package.json b/projects/demo-maps/package.json index 7ba2199db..bc4dbbc23 100644 --- a/projects/demo-maps/package.json +++ b/projects/demo-maps/package.json @@ -30,7 +30,8 @@ "@dlr-eoc/utils-maps": "12.0.0-alpha.2", "@dlr-eoc/services-ogc": "12.0.0-alpha.2", "@dlr-eoc/shared-assets": "12.0.0-alpha.2", - "@dlr-eoc/map-cesium": "12.0.0-alpha.2" + "@dlr-eoc/map-cesium": "12.0.0-alpha.2", + "@dlr-eoc/map-maplibre": "12.0.0-alpha.2" }, "devDependencies": { "zone.js": "^0.11.7", diff --git a/projects/demo-maps/src/app/app-routing.module.ts b/projects/demo-maps/src/app/app-routing.module.ts index 087d86600..c16257099 100644 --- a/projects/demo-maps/src/app/app-routing.module.ts +++ b/projects/demo-maps/src/app/app-routing.module.ts @@ -97,6 +97,15 @@ const routes: Routes = [ img: 'assets/route-cesium.jpg' } }, + { + path: 'maplibre', + loadChildren: () => import('./route-components/route-example-maplibre/route-example-maplibre.module').then(m => m.RouteExampleMaplibreModule), + data: { + title: 'Maplibre', + description: 'This example shows a maplibre map and how to work with UKIS layers', + img: 'assets/route-maplibre.jpg' + } + }, { path: 'licenses', loadChildren: () => import('./route-components/route-licenses/route-licenses.module').then(m => m.RouteLicensesModule), diff --git a/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.html b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.html new file mode 100644 index 000000000..6f9264ba1 --- /dev/null +++ b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.html @@ -0,0 +1,42 @@ +
+ +
+ + + + + + Overlays + + + + + + + + + Layers + + + + + + + + Baselayers + + + + + + + + Actions + + + + + + + + \ No newline at end of file diff --git a/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.scss b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.scss new file mode 100644 index 000000000..2c55cff34 --- /dev/null +++ b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.scss @@ -0,0 +1 @@ +@import 'maplibre-gl/dist/maplibre-gl.css'; \ No newline at end of file diff --git a/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.spec.ts b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.spec.ts new file mode 100644 index 000000000..7c2e5750b --- /dev/null +++ b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { RouteExampleMaplibreComponent } from './route-example-maplibre.component'; + +describe('RouteExampleMaplibreComponent', () => { + let component: RouteExampleMaplibreComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ RouteExampleMaplibreComponent ] + }) + .compileComponents(); + + fixture = TestBed.createComponent(RouteExampleMaplibreComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.ts b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.ts new file mode 100644 index 000000000..15edafd16 --- /dev/null +++ b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.component.ts @@ -0,0 +1,717 @@ +import { Component, HostBinding, OnDestroy, OnInit, ViewEncapsulation } from '@angular/core'; +import { CustomLayer, Layer, LayerGroup, LayersService, RasterLayer, StackedLayer, VectorLayer, WmsLayer, WmtsLayer } from '@dlr-eoc/services-layers'; +import { MapStateService } from '@dlr-eoc/services-map-state'; +import { MapMaplibreService } from '@dlr-eoc/map-maplibre'; +import { StyleSpecification, TerrainControl } from 'maplibre-gl'; + +import { OsmTileLayer, EocLitemap, BlueMarbleTile, EocBaseoverlayTile } from '@dlr-eoc/base-layers-raster'; +import greyscale from '@dlr-eoc/shared-assets/open-map-styles/open-map-style.json'; +import placeLabels from '@dlr-eoc/shared-assets/open-map-styles/open-map-style-place-labels.json'; +import testData from '@dlr-eoc/shared-assets/geojson/test.collection.json'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'app-route-example-maplibre', + templateUrl: './route-example-maplibre.component.html', + styleUrls: ['./route-example-maplibre.component.scss'], + // https://medium.com/@rishanthakumar/angular-lazy-load-common-styles-specific-to-a-feature-module-c3f81c40daf1 + encapsulation: ViewEncapsulation.None, + providers: [LayersService, MapStateService, MapMaplibreService] +}) +export class RouteExampleMaplibreComponent implements OnInit, OnDestroy { + @HostBinding('class') class = 'content-container'; + + subs: Subscription[] = []; + constructor( + public layerSvc: LayersService, + public mapStateSvc: MapStateService, + public mapSvc: MapMaplibreService) { } + + ngOnInit(): void { + this.addBaselayers(); + this.setMapState(); + this.setTerrain(); + + this.addlayers(); + this.addOverlays(); + + // this.subscribeToMapState(); + } + + ngOnDestroy() { + this.subs.map(s => s.unsubscribe()); + } + + setMapState() { + const zoom = 11; + const center = { + lat: 47.41449812198263, + lon: 11.7455863952639 + }; + this.mapStateSvc.setMapState({ + zoom, + center + }); + } + + setTerrain() { + // https://sparkgeo.com/blog/augmenting-mapbox-terrain/ + // https://blog.mapbox.com/global-elevation-data-6689f1d0ba65 + // https://github.com/tilezen/joerd/blob/master/docs/formats.md#terrarium + // https://water-gis.com/en/setups/terrain-rgb/create_terrainrgb/ + // https://github.com/syncpoint/terrain-rgb + // https://www.maptiler.com/news/2022/05/maplibre-2/ + const mapSub = this.mapSvc.map.subscribe(map => { + if (map) { + map.addSource('terrainSource', { + type: 'raster-dem', + encoding: "terrarium", // "mapbox", + tiles: [ + // "https://geoservice.dlr.de/eoc/test/wms?service=WMS&version=1.1.0&request=GetMap&layers=test%3ATDM90_DEM_Plus&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&styles=&format=image/png" + // "https://geoservice.dlr.de/eoc/basemap/gmted/wms?service=WMS&version=1.1.0&request=GetMap&layers=gmted%3Agmted&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&styles=&format=image/png" + // "https://sgx.geodatenzentrum.de/wms_dgm200?service=wms&version=1.3.0&request=GetMap&Layers=relief&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&styles=&format=image/png" + // "https://vtc-cdn.maptoolkit.net/terrainrgb/{z}/{x}/{y}.png" + // "https://wms.wheregroup.com/dem_tileserver/raster_dem/{z}/{x}/{y}.webp" + // "https://api.mapbox.com/raster/v1/mapbox.mapbox-terrain-dem-v1/{z}/{x}/{y}.webp", + + // https://registry.opendata.aws/terrain-tiles/ + "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" + ], + tileSize: 256, + attribution: `© AWS Terrain Tiles`, + minzoom: 3 + }); + const exaggeration = 1; // -0.001; // 0.001 // 1 //??? https://blog.mapbox.com/global-elevation-data-6689f1d0ba65 - height = -10000 + ((R * 256 * 256 + G * 256 + B) * 0.1) + map.setTerrain({ + source: 'terrainSource', + exaggeration: exaggeration + }); + + map.setBearing(-20); + map.setPitch(60); + map.setMaxPitch(80); + + map.addControl( + new TerrainControl({ + source: 'terrainSource', + exaggeration: exaggeration + }), 'top-left' + ); + } + }); + this.subs.push(mapSub); + } + + addBaselayers() { + // some of the fonts are not working + greyscale.layers.forEach(l => { + if (l?.layout?.['text-font']) { + l.layout['text-font'] = l.layout['text-font'].filter(i => i !== 'Noto Sans Regular' && i !== 'Noto Sans Italic'); + } + }); + + const layers = [ + new OsmTileLayer({ + visible: false + }), + new EocLitemap({ + visible: true + }), + new BlueMarbleTile({ + visible: false + }), + new VectorLayer({ + name: 'Transparenter Hintergrund', + id: 'blank_1', + type: 'geojson', + visible: false, + // maplibre needs a valid geojson object + data: { 'type': 'FeatureCollection', 'features': [] } + }), + new VectorLayer({ + name: 'Open Map Styles', + id: 'planet_eoc_vector_tiles', + attribution: `© OpenMapTiles © OpenStreetMap contributors`, + description: `EOC-Geoservice TMS-Service, Vector Tiles with OpenMapTiles and customised positron Style.`, + type: 'tms', + url: 'https://{s}.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true', + subdomains: ['a', 'b', 'c', 'd'], + options: { + style: greyscale, + styleSource: 'planet_eoc' + }, + visible: false + }) + ]; + + layers.map(l => this.layerSvc.addLayer(l, 'Baselayers')); + } + + addlayers() { + const eocBasemap = new WmsLayer({ + name: 'EOC Basemap', + displayName: 'EOC Basemap', + id: 'eoc_basemap', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'eoc:basemap', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://tiles.geoservice.dlr.de/service/wms', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://tiles.geoservice.dlr.de/service/wmts?layer=eoc%3Abasemap&style=_empty&tilematrixset=EPSG%3A3857&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A3857%3A5&TileCol=18&TileRow=11', + description: 'This is the basemap for DLR Service Portals', + opacity: 1 + }); + + const osm = new RasterLayer({ + name: 'OpenStreetMap', + displayName: 'OpenStreetMap', + id: 'osm_2', + visible: false, + type: 'xyz', + url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + attribution: '©, OpenStreetMap contributors', + continuousWorld: false, + legendImg: 'https://a.tile.openstreetmap.org/3/4/3.png', + description: 'OpenStreetMap z-x-y Tiles', + opacity: 1 + }); + + const eocLiteMap = new WmtsLayer({ + name: 'EOC Litemap Tile', + displayName: 'EOC Litemap Tile', + id: 'eoc_litemap_tile', + visible: false, + type: 'wmts', + removable: false, + params: { + layer: 'eoc:litemap', + format: 'image/png', + style: '_empty', + matrixSetOptions: { + matrixSet: 'EPSG:3857', + tileMatrixPrefix: 'EPSG:3857' + } + }, + url: 'https://tiles.geoservice.dlr.de/service/wmts', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://tiles.geoservice.dlr.de/service/wmts?layer=eoc%3Alitemap&style=_empty&tilematrixset=EPSG%3A3857&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A3857%3A5&TileCol=18&TileRow=11', + description: 'EOC Litemap as web map tile service', + opacity: 1 + }); + + const eocLiteOverlay = new EocBaseoverlayTile(); + + const MODIS_EU_DAILY = new WmsLayer({ + name: 'MODIS EU Daily', + id: 'MODIS_EU_DAILY', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'MODIS_EU_DAILY', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://geoservice.dlr.de/eoc/imagery/wms', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://geoservice.dlr.de/eoc/imagery/wms?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=MODIS_EU_DAILY', + opacity: 1 + }); + + // https://sgx.geodatenzentrum.de/wms_sen2europe?service=wms&version=1.3.0&request=GetMap&Layers=sentinel2-de:rgb&STYLES=&CRS=EPSG:25832&bbox=500000,5700000,550000,5750000&width=500&Height=500&Format=image/png&TIME=2018 + const sentinel2Europe = new WmsLayer({ + name: 'Sentinel-2 Europe', + id: 'sentinel2Europe', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'rgb', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://sgx.geodatenzentrum.de/wms_sen2europe', + attribution: '©, Europäische Union - BKG', + continuousWorld: false, + legendImg: 'https://sgx.geodatenzentrum.de/wms_sen2europe?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=rgb', + opacity: 1 + }); + + const mosaic_hillshade = new WmsLayer({ + name: 'mosaic_hillshade', + id: 'gmted2010_dsc075_mosaic_hillshade', + visible: true, + type: 'wms', + removable: false, + params: { + LAYERS: 'gmted2010_dsc075_mosaic_hillshade', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://geoservice.dlr.de/eoc/basemap/wms', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://geoservice.dlr.de/eoc/basemap/wms?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=gmted%3Agmted2010_dsc075_mosaic_hillshade', + opacity: 0.5 + }); + + const waterway = new CustomLayer({ + id: 'waterway-planet_eoc', + name: 'waterway', + visible: true, + removable: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + 'waterway-planet_eoc': // 'planet_eoc': + { + "type": "vector", + "__Comment": "The url to the tilejson is not public available so we use the tiles array to skip the request, to make use of the tms service. See https://github.com/openlayers/ol-mapbox-style/blob/v8.2.1/src/util.js#L109", + "url": "", + "tiles": [ + "https://a.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://b.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://c.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://d.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true" + ] + } + }, + layers: [ + { + "id": "water", + "type": "fill", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "hsl(198, 100%, 28%)" + } + }, + { + "id": "waterway", + "type": "line", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(198, 100%, 28%)" + }, + // ignore set visibility on ukisLayer change + "metadata": { + "ukis:ignore-visibility": true + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Medium Italic", + // "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "rgb(157,169,177)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + } + ] + } + }); + + + const hillshade = new CustomLayer({ + id: 'hillshade_raster_dem', + name: 'hillshade raster dem', + visible: false, + removable: true, + attribution: `© AWS Terrain Tiles`, + custom_layer: { + version: 8, + sources: { + hillshadeSource: { + "type": "raster-dem", + "encoding": "terrarium", //"mapbox", + "tileSize": 512, // 256 + "tiles": [ + // "https://geoservice.dlr.de/eoc/test/wms?service=WMS&version=1.1.0&request=GetMap&layers=test%3ATDM90_DEM_Plus&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&styles=&format=image/png" + // "https://geoservice.dlr.de/eoc/basemap/gmted/wms?service=WMS&version=1.1.0&request=GetMap&layers=gmted%3Agmted&bbox={bbox-epsg-3857}&width=256&height=256&srs=EPSG:3857&styles=&format=image/png" + // "https://vtc-cdn.maptoolkit.net/terrainrgb/{z}/{x}/{y}.png" + "https://s3.amazonaws.com/elevation-tiles-prod/terrarium/{z}/{x}/{y}.png" + ], + minzoom: 3 + } + }, + layers: [ + { + id: 'hills', + type: 'hillshade', + source: 'hillshadeSource', + layout: { visibility: 'visible' }, + paint: { + 'hillshade-shadow-color': '#473B24', //'#000000', + /* 'hillshade-accent-color': '#9b9b9b', + 'hillshade-highlight-color': '#FFFFFF', + 'hillshade-illumination-anchor': 'map', + 'hillshade-illumination-direction': 335, */ + } + } + ] + } + }); + + // https://docs.geoserver.org/latest/en/user/extensions/vectortiles/index.html + const customTMSGeoserver = new CustomLayer({ + id: 'geoserverCountries', + name: 'geoserverCountries', + visible: false, + removable: true, + custom_layer: { + version: 8, + sources: { + geoserverCountries: { + type: 'vector', + tiles: [ + "http://localhost:8080/geoserver/gwc/service/tms/1.0.0/ne%3Acountries@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true" + ], + tileSize: 512 + } + }, + layers: [ + { + "id": "geoserverCountries", + "type": "fill", + "source": "geoserverCountries", + "source-layer": "countries", // name of the layer in geoserver + 'filter': [ + "all", + [ + "!=", + "NAME", // geoserver Feature Type Details -> Properties + "Germany" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "hsl(198, 100%, 28%)" + } + }, + { + "id": "geoserverCountriesLine", + "type": "line", + "source": "geoserverCountries", + "source-layer": "countries", + 'filter': [ + "all", + [ + "==", + "NAME", // geoserver Feature Type Details -> Properties + "Germany" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(25, 100%, 50%)" + } + } + ] + } + }); + + const geoJsonLayer = new VectorLayer({ + id: 'geojson_test', + name: 'GeoJSON Vector Layer', + attribution: `© DLR GeoJSON`, + type: 'geojson', + data: testData, + bbox: [5.461, 8.631, 53.931, 42.193], + visible: false + }); + + + const wfsLayer = new VectorLayer({ + id: 'WfsLayer', + name: 'Coastline (WFS)', + type: 'wfs', + visible: false, + url: "https://geoservice.dlr.de/eoc/basemap/wfs?service=WFS&request=GetFeature&outputFormat=application/json&version=1.1.0&srsname=EPSG:4326&typenames=ne:ne_50m_coastline", // &cql_filter=STATE_NAME='Pennsylvania' + }); + + + const kmlLayer = new VectorLayer({ + id: 'ID-ukis-kml', + name: 'TimeZones (KML)', + type: 'kml', + data: 'assets/kml/TimeZones.kml', + visible: false + }); + + const agrodeLayer = new WmsLayer({ + type: 'wms', + id: 'S2_L3A_WASP_FRC_P1M', + url: 'https://{s}.geoservice.dlr.de/eoc/imagery/wms', + name: 'Sentinel-2 L3A FRC (WASP)', + visible: false, + subdomains: ['a', 'b', 'c', 'd'], + filtertype: 'Layers', + attribution: '© DLR Contains modified Copernicus Sentinel Data [2020]', + params: { + LAYERS: 'S2_L3A_WASP_FRC_P1M', + VERSION: '1.1.0', + FORMAT: 'image/png', + }, + expanded: { + tab: 'settings' + }, + bbox: [2.183, 47.076, 8.206, 49.287], + styles: [ + { + default: true, + legendURL: 'https://geoservice.dlr.de/eoc/imagery/wms?service=WMS&request=GetLegendGraphic&format=image/png&width=20&height=20&layer=land:S2_L3A_WASP_FRC_P1M', + name: 's2-ndvi', + title: 'NDVI' + }, + { + default: false, + legendURL: 'https://geoservice.dlr.de/eoc/imagery/wms?service=WMS&request=GetLegendGraphic&format=image/png&width=20&height=20&layer=land:S2_L3A_WASP_FRC_P1M', + name: 's2-infrared', + title: 'Infrared (8,4,3)' + }, + { + default: false, + legendURL: 'https://geoservice.dlr.de/eoc/imagery/wms?service=WMS&request=GetLegendGraphic&format=image/png&width=20&height=20&layer=land:S2_L3A_WASP_FRC_P1M', + name: 's2-l3a-wasp-frc', + title: 'Style for L3A MAJA/WASP Ground Reflectances' + } + ] + }); + + + const stackedLayer = new StackedLayer({ + id: 'stackedLayer_id', + name: 'EocLiteMap And Overlay', + description: 'merged/stacked Layers EOC Lite with Overlay', + layers: [eocLiteMap, eocLiteOverlay], + visible: false + }); + + const groupLayer = new LayerGroup({ + id: 'group_1', + name: 'Raster Group', + visible: false, + layers: [eocBasemap, osm, MODIS_EU_DAILY, sentinel2Europe], + description: 'This is a group with multiple raster layers', + expanded: { + tab: 'description' + }, + actions: [{ title: 'download', icon: 'download-cloud', action: (group) => { console.log(group); } }] + }); + + + const layers = [groupLayer, hillshade, mosaic_hillshade, waterway, wfsLayer, kmlLayer, geoJsonLayer, stackedLayer, agrodeLayer]; + layers.map(l => { + if (l instanceof Layer) { + this.layerSvc.addLayer(l, 'Layers'); + } else { + this.layerSvc.addLayerGroup(l, 'Layers'); + } + }); + } + + addOverlays() { + const eocLitemapOverlay = new WmsLayer({ + name: 'EOC Liteoverlay', + displayName: 'EOC Liteoverlay', + id: 'eoc_Liteoverlay', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'eoc:liteoverlay', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://tiles.geoservice.dlr.de/service/wms', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://tiles.geoservice.dlr.de/service/wmts?layer=eoc%3Aliteoverlay&style=_empty&tilematrixset=EPSG%3A3857&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A3857%3A5&TileCol=18&TileRow=11', + description: 'This is the liteoverlay provided for EOC Service Portals', + opacity: 1 + }); + + const geonamesCities = new WmsLayer({ + name: 'Geonames cities', + displayName: 'Geonames cities', + id: 'gn_cities', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'gn:cities', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://geoservice.dlr.de/eoc/basemap/wms', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://geoservice.dlr.de/eoc/basemap/wms?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=gn%3Acities', + opacity: 1 + }); + + const admin0countries = new WmsLayer({ + name: 'admin 0 countries', + displayName: 'admin 0 countries', + id: 'ne_10m_admin_0_countries', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'ne:ne_10m_admin_0_countries', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://geoservice.dlr.de/eoc/basemap/wms', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://geoservice.dlr.de/eoc/basemap/wms?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=ne%3Ane_10m_admin_0_countries', + opacity: 1 + }); + + // some of the fonts are not working + placeLabels.layers.forEach(l => { + l.source = 'place-labels-planet_eoc'; + if (l?.layout?.['text-font']) { + l.layout['text-font'] = l.layout['text-font'].filter(i => i !== 'Noto Sans Regular' && i !== 'Noto Sans Italic'); + } + }); + const labels = new CustomLayer({ + id: 'place-labels-planet_eoc', + name: 'Place Labels', + visible: true, + removable: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + 'place-labels-planet_eoc': + { + "type": "vector", + "__Comment": "The url to the tilejson is not public available so we use the tiles array to skip the request, to make use of the tms service. See https://github.com/openlayers/ol-mapbox-style/blob/v8.2.1/src/util.js#L109", + "url": "", + "tiles": [ + "https://a.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://b.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://c.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://d.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true" + ] + } + }, + layers: placeLabels.layers + } + }) + + const overlays = [eocLitemapOverlay, geonamesCities, admin0countries, labels]; + overlays.map(l => this.layerSvc.addLayer(l, 'Overlays')); + } + + subscribeToMapState() { + const mapStatSub = this.mapStateSvc.getMapState().subscribe((state) => { + console.log({ zoom: state.zoom.toString(), center: `${state.center.lat},${state.center.lon}` }); + }); + this.subs.push(mapStatSub); + } + + updateLayer() { + const layer = this.layerSvc.getLayerOrGroupById('geojson_test') as unknown as VectorLayer; + layer.data = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "properties": {}, + "geometry": { + "coordinates": [ + [ + [ + 11.771870735772268, + 47.49013323424285 + ], + [ + 11.771870735772268, + 47.44101685032831 + ], + [ + 11.85227430395085, + 47.44101685032831 + ], + [ + 11.85227430395085, + 47.49013323424285 + ], + [ + 11.771870735772268, + 47.49013323424285 + ] + ] + ], + "type": "Polygon" + } + } + ] + }; + this.layerSvc.updateLayer(layer); + } + +} diff --git a/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.module.ts b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.module.ts new file mode 100644 index 000000000..d84dec628 --- /dev/null +++ b/projects/demo-maps/src/app/route-components/route-example-maplibre/route-example-maplibre.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouteExampleMaplibreComponent } from './route-example-maplibre.component'; +import { MapMaplibreModule } from '@dlr-eoc/map-maplibre'; +import { LayerControlModule } from '@dlr-eoc/layer-control'; +import { RouterModule, Routes } from '@angular/router'; +import { SharedComponentsModule } from '../../app-shared-components.module'; +import { ClarityModule } from '@clr/angular'; + + +const routes: Routes = [{ path: '', component: RouteExampleMaplibreComponent }]; +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class RouteExampleMaplibreRoutingModule { } + +@NgModule({ + declarations: [ + RouteExampleMaplibreComponent + ], + imports: [ + CommonModule, + SharedComponentsModule, + RouteExampleMaplibreRoutingModule, + + ClarityModule, + LayerControlModule, + MapMaplibreModule, + ] +}) +export class RouteExampleMaplibreModule { } diff --git a/projects/demo-maps/src/assets/route-maplibre.jpg b/projects/demo-maps/src/assets/route-maplibre.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1aabff6c4abaa539d5dc573afcc516bc340df088 GIT binary patch literal 44940 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 + + +``` + +#### add the following to a route-view.component.ts +``` +import { LayersService } from '@dlr-eoc/services-layers'; +import { MapStateService } from '@dlr-eoc/services-map-state'; +import { MapMaplibreService } from '@dlr-eoc/map-maplibre'; + +import { OsmTileLayer, EocLitemap, BlueMarbleTile } from '@dlr-eoc/base-layers-raster'; +``` + + +``` +constructor( + public layerSvc: LayersService, + public mapStateSvc: MapStateService, + public mapSvc: MapMaplibreService) { } +``` + +``` +// add a OnInit Function +export class implements OnInit... +``` + +``` +ngOnInit() { + this.addBaselayers(); +} + +addBaselayers() { + const layers = [ + new OsmTileLayer({ + visible: false + }), + new EocLitemap({ + visible: true + }), + new BlueMarbleTile({ + visible: false + }) + ]; + + layers.map(l => this.layerSvc.addLayer(l, 'Baselayers')); +} +``` + + +## TODO +- There are currently no popups implemented for layers + +- Some properties of Layers or Layergroups are currently not used. E.g. +- `Layer.continuousWorld`: this is not available in maplibre +- `Layer.minResolution`: this is not available in maplibre use minZoom +- `Layer.maxResolution`: this is not available in maplibre use maxZoom +- `Layer.bbox`: Works only with some sources see https://maplibre.org/maplibre-style-spec/sources/#sources - bounds +- `Layer.events`: TODO: https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.StyleLayer/ on/of +- `Layer.crossOrigin`: this is not available in maplibre + +=== + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.2.0. + +## Code scaffolding + +Run `ng generate component component-name --project map-maplibre` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module --project map-maplibre`. +> Note: Don't forget to add `--project map-maplibre` or else it will be added to the default project in your `angular.json` file. + +## Build + +Run `ng build map-maplibre` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Publishing + +After building your library with `ng build map-maplibre`, go to the dist folder `cd dist/map-maplibre` and run `npm publish`. + +## Running unit tests + +Run `ng test map-maplibre` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Further help + +To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. diff --git a/projects/map-maplibre/karma.conf.js b/projects/map-maplibre/karma.conf.js new file mode 100644 index 000000000..e628505b8 --- /dev/null +++ b/projects/map-maplibre/karma.conf.js @@ -0,0 +1,53 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html +const PATH = require('path'); + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + // // https://karma-runner.github.io/6.3/config/files.html#loading-assets + files: [ + { pattern: '../shared-assets/**', watched: false, included: false, served: true }, + ], + // https://github.com/karma-runner/karma/issues/2703#issuecomment-421987843 + proxies: { + '/assets/': `/absolute${PATH.normalize(PATH.resolve('projects/shared-assets/'))}` + }, + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/map-maplibre'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/projects/map-maplibre/ng-package.json b/projects/map-maplibre/ng-package.json new file mode 100644 index 000000000..a5f290af9 --- /dev/null +++ b/projects/map-maplibre/ng-package.json @@ -0,0 +1,15 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/map-maplibre", + "lib": { + "entryFile": "src/public-api.ts" + }, + "allowedNonPeerDependencies": [ + "tslib", + "maplibre-gl", + "@mapbox/togeojson", + "@dlr-eoc/services-layers", + "@dlr-eoc/services-map-state", + "@dlr-eoc/utilities" + ] +} \ No newline at end of file diff --git a/projects/map-maplibre/package.json b/projects/map-maplibre/package.json new file mode 100644 index 000000000..748886f29 --- /dev/null +++ b/projects/map-maplibre/package.json @@ -0,0 +1,33 @@ +{ + "name": "@dlr-eoc/map-maplibre", + "version": "12.0.0-alpha.2", + "main": "src/public-api", + "license": "Apache-2.0", + "author": "Team UKIS", + "description": "This is a angular module that exports a maplibre-gl component that can handle UKIS layers. See @dlr-eoc/services-layers for supported types.", + "keywords": [ + "angular", + "mapping", + "maplibre-gl", + "maplibre", + "layers" + ], + "peerDependencies": { + "@angular/common": "^14.2.0", + "@angular/core": "^14.2.0", + "rxjs": "^6.6.7" + }, + "dependencies": { + "tslib": "^2.3.0", + "maplibre-gl": "^3.3.0", + "@mapbox/togeojson": "0.16.0", + "@dlr-eoc/services-map-state": "12.0.0-alpha.2", + "@dlr-eoc/services-layers": "12.0.0-alpha.2", + "@dlr-eoc/utilities": "12.0.0-alpha.2" + }, + "devDependencies": { + "@dlr-eoc/base-layers-raster": "12.0.0-alpha.2", + "@dlr-eoc/shared-assets": "12.0.0-alpha.2", + "ol": "^7.3.0" + } +} \ No newline at end of file diff --git a/projects/map-maplibre/src/lib/map-maplibre.component.html b/projects/map-maplibre/src/lib/map-maplibre.component.html new file mode 100644 index 000000000..120a58c6e --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.component.html @@ -0,0 +1 @@ +

\ No newline at end of file diff --git a/projects/map-maplibre/src/lib/map-maplibre.component.scss b/projects/map-maplibre/src/lib/map-maplibre.component.scss new file mode 100644 index 000000000..e75c9c373 --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.component.scss @@ -0,0 +1,69 @@ +/** + * depends on 'maplibre-gl/dist/maplibre-gl.css'; + */ + +:root { + --ukis-popup-bg-color: rgb(238, 238, 238); + --ukis-drop-shadow: drop-shadow(0 1px 4px rgba(0, 0, 0, 0.2)); + --ukis-gl-bbox-bg-color: rgba(255, 255, 255, 0.4); + --ukis-gl-bbox-border-color: rgba(87, 87, 87, 0.4); + --ukis-gl-overviewmap-left: 0.5em; + --ukis-gl-overviewmap-bottom: 3.0em; + --ukis-gl-control-bg-color: rgba(87, 87, 87, 0.6); + --ukis-gl-control-border-color: rgba(87, 87, 87, 0.4); +} + + +.map { + width: 100%; + height: 100%; //calc(100% - 56px); //header 50 + 2 + position: relative; +} + +//restyle Controlls +.maplibregl-control-container { + .maplibregl-ctrl-group button { + background-color: var(--ukis-gl-control-bg-color); + color: #fff; + border-radius: 2px; + + &:focus { + background-color: var(--ukis-gl-control-bg-color); + } + + &:hover { + background-color: var(--ukis-gl-control-border-color); + } + } +} + +.maplibregl-ctrl-group:not(:empty) { + box-shadow: 0 0 0 2px rgb(255 255 255 / 81%); +} + +.maplibregl-ctrl button.maplibregl-ctrl-zoom-in .maplibregl-ctrl-icon { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M14.5 8.5c-.75 0-1.5.75-1.5 1.5v3h-3c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h3v3c0 .75.75 1.5 1.5 1.5S16 19.75 16 19v-3h3c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-3v-3c0-.75-.75-1.5-1.5-1.5z'/%3E%3C/svg%3E"); +} + +.maplibregl-ctrl button.maplibregl-ctrl-zoom-out .maplibregl-ctrl-icon { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='M10 13c-.75 0-1.5.75-1.5 1.5S9.25 16 10 16h9c.75 0 1.5-.75 1.5-1.5S19.75 13 19 13h-9z'/%3E%3C/svg%3E"); +} + +.maplibregl-ctrl button.maplibregl-ctrl-compass .maplibregl-ctrl-icon { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='29' height='29' fill='%23fff' viewBox='0 0 29 29'%3E%3Cpath d='m10.5 14 4-8 4 8h-8z'/%3E%3Cpath fill='%23000' d='m10.5 16 4 8 4-8h-8z'/%3E%3C/svg%3E"); +} + +.maplibregl-ctrl button.maplibregl-ctrl-terrain-enabled .maplibregl-ctrl-icon { + background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='22' height='22' fill='%23fff' viewBox='0 0 22 22'%3E%3Cpath d='m1.754 13.406 4.453-4.851 3.09 3.09 3.281 3.277.969-.969-3.309-3.312 3.844-4.121 6.148 6.886h1.082v-.855l-7.207-8.07-4.84 5.187L6.169 6.57l-5.48 5.965v.871ZM.688 16.844h20.625v1.375H.688Zm0 0'/%3E%3C/svg%3E"); +} + + +.maplibregl-ctrl-scale { + box-shadow: 0 0 0 2px rgb(255 255 255 / 30%); + background: var(--ukis-gl-control-bg-color); + line-height: 1.575em; + padding: 1px; + border: 1px solid #666666; + color: #fff; + text-align: center; +} \ No newline at end of file diff --git a/projects/map-maplibre/src/lib/map-maplibre.component.spec.ts b/projects/map-maplibre/src/lib/map-maplibre.component.spec.ts new file mode 100644 index 000000000..63c11a266 --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.component.spec.ts @@ -0,0 +1,87 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MapMaplibreComponent } from './map-maplibre.component'; +import { MapMaplibreService } from './map-maplibre.service'; +import { MapStateService } from '@dlr-eoc/services-map-state'; +import { LayersService } from '@dlr-eoc/services-layers'; +import { Map } from 'maplibre-gl'; +import { EocBasemapTile, OsmTileLayer, EocBaseoverlayTile } from '@dlr-eoc/base-layers-raster'; + + +function addSomeLayers(component: MapMaplibreComponent, mapSvc: MapMaplibreService) { + /* + * Unfortunately, component.subscribeToLayers() does not work in the test, so we have to add the layers manually instead of using layersSvc. + */ + /** + * baseLayers.forEach(l => { + component.layersSvc.addLayer(l, 'Baselayers'); + }); + */ + + const layers = [new OsmTileLayer(), new EocBaseoverlayTile(), new EocBasemapTile()]; + //@ts-ignore use of private function + component.addUpdateLayers(layers.filter(l => mapSvc.layerIsSupported(l)), 'Layers'); + + // mapSvc.setUkisLayers(baseLayers, 'Baselayers', component.map); + return { + layers + }; +} + +/** + * Unfortunately running tests in watch with edit does not work + * + * -> Async function did not complete within 5000ms + */ +describe('MapMaplibreComponent', () => { + let component: MapMaplibreComponent; + let fixture: ComponentFixture; + let mapSvc: MapMaplibreService; + const mapSize = [1024, 768]; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [MapMaplibreComponent], + providers: [ + MapMaplibreService, + { provide: LayersService, useClass: LayersService }, + { provide: MapStateService, useClass: MapStateService } + ] + }).compileComponents(); + + fixture = TestBed.createComponent(MapMaplibreComponent); + component = fixture.componentInstance; + component.layersSvc = new LayersService(); + component.mapStateSvc = new MapStateService(); + mapSvc = TestBed.inject(MapMaplibreService); + fixture.detectChanges(); + component.map._container.style.height = `${mapSize[1]}px`; + component.map._container.style.width = `${mapSize[0]}px`; + component.map.resize(); + fixture.detectChanges(); + + // https://github.com/maplibre/maplibre-gl-js/discussions/2193 + await new Promise((resolve, reject) => { + // idle + component.map.once('load', (evt) => { + resolve(evt); + }); + }); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should have a map and the map should be the same as from the mapSvc', () => { + expect(component.map instanceof Map).toBeTruthy(); + expect(mapSvc.map.getValue()._mapId).toBe(component.map._mapId); + }); + + it('should add/update Layers to the style', () => { + const { layers } = addSomeLayers(component, mapSvc); + const mapStyle = component.map.getStyle(); + expect(mapStyle.layers.length).toBe(layers.length); + expect(mapStyle.metadata[`ukis:LayersIDs`]).toEqual(layers.map(l => l.id)); + }); +}); diff --git a/projects/map-maplibre/src/lib/map-maplibre.component.ts b/projects/map-maplibre/src/lib/map-maplibre.component.ts new file mode 100644 index 000000000..17ebd893d --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.component.ts @@ -0,0 +1,401 @@ +import { AfterViewChecked, AfterViewInit, Component, ElementRef, Input, NgZone, OnDestroy, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Map as glMap, MapLibreEvent, NavigationControl, ScaleControl, StyleSpecification, TypedStyleLayer, GeoJSONSource, Dispatcher, Evented } from 'maplibre-gl'; +import { setExtent, setCenter, setZoom, getExtent, getAllLayers, getUkisLayerIDs, removeLayerAndSource, UKIS_METADATA, changeOrderOfLayers } from './maplibre.helpers'; + +import { MapState, MapStateService } from '@dlr-eoc/services-map-state'; +import { LayersService, TFiltertypes, TFiltertypesUncap, Layer as ukisLayer } from '@dlr-eoc/services-layers'; + + +import { Subject, Subscription } from 'rxjs'; +import { combineLatest, delay } from 'rxjs/operators'; +import { MapMaplibreService } from './map-maplibre.service'; +import { getUkisLayerMetadata } from './maplibre-layers.helpers'; +import toGeoJson from '@mapbox/togeojson'; + +type Tgroupfiltertype = TFiltertypesUncap | TFiltertypes; + +/** + * This has to be global, because maplibre does this the same way + * https://github1s.com/maplibre/maplibre-gl-js/blob/main/src/source/source.ts#L18-L19 + */ +const hasSourceType = {}; + +@Component({ + selector: 'ukis-map-maplibre', + templateUrl: './map-maplibre.component.html', + styleUrls: ['./map-maplibre.component.scss'], + encapsulation: ViewEncapsulation.None +}) +export class MapMaplibreComponent implements OnInit, AfterViewInit, AfterViewChecked, OnDestroy { + @Input('layersSvc') layersSvc!: LayersService; + @Input('mapState') mapStateSvc!: MapStateService; + + @ViewChild('mapDiv') mapDivView!: ElementRef; + map!: glMap + subs: Subscription[] = []; + + mapCreated = new Subject(); + + /** [width, height] */ + public mapSize = [0, 0]; + private initialMapStateSet = false; + private initialMapState: MapState | null = null; + constructor(private ngZone: NgZone, private mapSvc: MapMaplibreService) { } + + ngOnInit(): void { + if (!this.layersSvc) { + console.error(`provide a LayersService as Input to ukis-map-leaflet`); + } + if (!this.mapStateSvc) { + console.error(`provide a MapStateService as Input to ukis-map-leaflet`); + } + + /** Subscribe to mapStateSvc before map is created */ + this.subscribeToMapState(); + + /** subscribe to layers oninit so they get pulled after view init */ + this.subscribeToLayers(); + } + + ngAfterViewInit(): void { + this.initMap(); + + /** Subscribe to map events when the map is completely created */ + this.subscribeToMapEvents(); + // this.map.getTargetElement().addEventListener('mouseleave', this.removePopupsOnMouseLeave); + } + + ngAfterViewChecked() { + /** + * compare map and container size to update Map Size on container resize + */ + this.updateMapSize(); + } + + ngOnDestroy(): void { + if (this.map) { + this.map.off('moveend', this.mapOnMoveend); + // this.mapDivView.nativeElement.removeEventListener('mouseleave', this.removePopupsOnMouseLeave); + } + } + + /** + * https://maplibre.org/maplibre-gl-js-docs/api/ + * + * https://github.com/maplibre/ngx-maplibre-gl + * + * */ + private initMap() { + // https://github.com/maptiler/angular-template-maplibre-gl-js/blob/master/src/app/map/map.component.ts + // zone? : https://github.com/Wykks/ngx-mapbox-gl/blob/main/libs/ngx-mapbox-gl/src/lib/map/map.service.ts#L104 + // NgZone.assertNotInAngularZone(); + + // for font styles: The easiest way to turn your custom fonts into files compatible with maplibre-gl - https://github.com/maplibre/font-maker + + const baseStyle: StyleSpecification = { + "version": 8, + "name": "Merged Style Specifications", + "metadata": { + }, + "sources": {}, + "sprite": "https://openmaptiles.github.io/positron-gl-style/sprite", + "glyphs": "http://fonts.openmaptiles.org/{fontstack}/{range}.pbf", + "layers": [] + }; + + this.map = new glMap({ + container: this.mapDivView.nativeElement, + style: baseStyle as StyleSpecification + }); + + this.addCustomSources(); + this.setControls(); + + if (!this.layersSvc) { + console.log('there is no layersSvc as defined!'); + } + + if (!this.mapStateSvc) { + console.log('there is no mapStateSvc as defined!'); + } + + + + this.map.once('load', () => { + // first wait till map and style load then layers can be add + this.mapCreated.next(true); + this.mapSvc.map.next(this.map); + }); + } + + private addCustomSources() { + if (!hasSourceType['kml']) { + this.addKmlSourceType(); + hasSourceType['kml'] = true; + } + } + + private addKmlSourceType() { + /** + * add custom source + * https://github.com/maplibre/maplibre-gl-js/blob/4619234968089ee67f761bde6ce24e1f861fb8c6/src/source/geojson_source.ts#L266 + * https://github.com/jimmyrocks/mapbox-gl-custom-protocol/blob/main/src/index.ts#L44 + * https://github.com/indus/mapsrc/blob/main/packages/TOPO/src/mapsrcTOPO.ts + * https://github.com/mapbox/mapbox-gl-js/issues/2920 + */ + const FeatureCollection = { 'type': 'FeatureCollection', 'features': [] }; + class KMLSource extends GeoJSONSource { + constructor(id: string, { data, ...options }: any, dispatcher: Dispatcher, eventedParent: Evented) { + super(id, Object.assign(options, { data: FeatureCollection }), dispatcher, eventedParent); + this.id = id; + this.type = "geojson"; + this._options.data = data; + this._preSetData(data); + } + + setData(data: string) { + this._preSetData(data); + super.setData(this._data); + return this; + } + + /** kml string or url */ + _preSetData(data: string) { + if (typeof data === 'string' && data.includes('.kml')) { + var req = new XMLHttpRequest(); + req.open("GET", data); + req.responseType = "text"; + req.addEventListener("load", () => this.setData(req.response)); + req.send(); + } else if (data.includes(' registeredSources[name] = SourceType + // getSourceType -> return registeredSources[name] + this.map.addSourceType('kml', KMLSource, (err, result) => { + if (err) { + console.log(err, result); + } + }); + } + + private setControls() { + this.map.setMaxPitch(75); + + const nav = new NavigationControl({ + showCompass: true, + showZoom: true, + visualizePitch: true + }); + this.map.addControl(nav, 'top-left'); + + const scale = new ScaleControl({ + unit: 'metric' + }); + this.map.addControl(scale, 'bottom-left'); + + + /* const attribution = new AttributionControl({ + compact: false + }); + this.map.addControl(attribution, 'bottom-right'); */ + } + + + private subscribeToMapEvents() { + this.map.on('moveend', this.mapOnMoveend); + + /** + * TODO: Popups + * handle click and pointermove/mousemove + */ + + /** + * TODO: + * handle double click + */ + } + + private mapOnMoveend = (evt: MapLibreEvent) => { + const zoom = this.map.getZoom(); + const latLng = this.map.getCenter(); + const extent = getExtent(this.map, true); + + const newCenter = { lat: latLng.lat, lon: latLng.lng }; + const ms = new MapState(zoom, newCenter, { notifier: 'map' }, extent); + this.mapStateSvc.setMapState(ms); + }; + + private updateMapSize() { + const mapDiv = this.getMapDiv(); + if (mapDiv) { + if (mapDiv.width === this.mapSize[0] && mapDiv.height === this.mapSize[1]) { + if (!this.initialMapStateSet && this.initialMapState) { + /** + * If container size and map size are equal (map size 'stable') + * Get last state from mapStateSvc and set it, so a User can set the initial MapState in a component on ngOnInit + * Update map size before so view.fit can calculate correct center + */ + this.setMapState(this.initialMapState); + this.initialMapStateSet = true; + } + } else { + /** update map size till container size and map size are equal */ + this.ngZone.runOutsideAngular(() => { + // resize triggers setMapState so mapStateSvc.getLastAction().getValue() was incorrect -> now use the initialMapState + this.map.resize(); + const container = this.map.getContainer(); + this.mapSize = [container.clientWidth, container.clientHeight]; + }); + } + + } + } + + private setMapState(mapState: MapState) { + if (!this.initialMapState) { + this.initialMapState = mapState; + } + const lastAction = this.mapStateSvc.getLastAction().getValue(); + if (mapState.options.notifier === 'user' && this.map) { + if (lastAction === 'setExtent') { + setExtent(this.map, mapState.extent, true); + } else if (lastAction === 'setState') { + setZoom(this.map, mapState.zoom, mapState.options.notifier); + setCenter(this.map, [mapState.center.lon, mapState.center.lat], true); + } + } + } + + private getMapDiv() { + if (this.mapDivView && this.mapDivView.nativeElement) { + return { + width: this.mapDivView.nativeElement.offsetWidth, + height: this.mapDivView.nativeElement.offsetHeight + } + } else { + return null; + } + } + + private subscribeToMapState() { + if (this.mapStateSvc) { + const mapStateOn = this.mapStateSvc.getMapState().subscribe(item => this.setMapState(item)); + this.subs.push(mapStateOn); + } + } + + // -------------------------------------------------- + + private subscribeToLayers() { + // add and remove layers + if (this.layersSvc) { + /** + * use delay https://blog.angular-university.io/angular-debugging/#analternativeusingrxjs + * Expression has changed after it was checked + * -> addBaseLayers changes visible in layers array + * -> better try to create layers before the map is created, but add them when the map is created. + * + * combineLatest is replaced with combineLatestWith in rxjs v7.x. Wait until Angular supports this. + */ + const onBaselayers = this.mapCreated.asObservable().pipe(delay(0), (combineLatest(this.layersSvc.getBaseLayers()))) + .subscribe(obs => this.addUpdateBaseLayers(obs[1].filter(l => this.mapSvc.layerIsSupported(l)))); + this.subs.push(onBaselayers); + + const onLayers = this.mapCreated.asObservable().pipe(delay(0), (combineLatest(this.layersSvc.getLayers()))) + .subscribe(obs => this.addUpdateLayers(obs[1].filter(l => this.mapSvc.layerIsSupported(l)), 'layers')); + this.subs.push(onLayers); + + const onOverlays = this.mapCreated.asObservable().pipe(delay(0), (combineLatest(this.layersSvc.getOverlays()))) + .subscribe(obs => this.addUpdateLayers(obs[1].filter(l => this.mapSvc.layerIsSupported(l)), 'overlays')); + this.subs.push(onOverlays); + } + } + + + + private addUpdateBaseLayers(layers: ukisLayer[]) { + const filtertype = 'baselayers'; + // this is like map change style but we only like to update nor recreat alle style + // map.setStyle(): https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.Map/#setstyle + // Changes in sprites and glyphs cannot be diffed. + const visiblelayers = layers.filter(i => i.visible); + + + /** if length of layers has changed add new layers */ + const mapLayers = getUkisLayerIDs(this.map, filtertype); + + if (layers.length !== mapLayers.length) { + // set only one visible at start + if (visiblelayers.length === 0) { + layers[0].visible = true; + } else if (visiblelayers.length > 1) { + layers.forEach(l => l.visible = false); + layers[0].visible = true; + } + + this.mapSvc.setUkisLayers(layers, filtertype, this.map); + } else { + /** if layers already on the map -length not changed- update them */ + this.updateLayers(layers, mapLayers, filtertype); + } + // console.log(layers, 'visible', visiblelayers, 'map -', mapLayers, 'map visible -', visibleMapLayers) + } + + private addUpdateLayers(layers: ukisLayer[], filtertype: Tgroupfiltertype) { + // this.map.addSource or update (this.map.removeLayer and this.map.addLayer) + // and this.map.addLayer or update (this.map.removeLayer and this.map.addLayer) + + + /** if length of layers has changed add new layers */ + const mapLayers = getUkisLayerIDs(this.map, filtertype); + + if (layers.length !== mapLayers.length) { + const layerIDs = layers.map(l => l.id); + const removedLayers = mapLayers.filter(l => layerIDs.indexOf(l) === -1); + + //TODO: if layer was StyleSpecification how to remove all things from it + removeLayerAndSource(this.map, removedLayers); + // console.log(`reset ${filtertype}`, layers, mapLayers); + this.mapSvc.setUkisLayers(layers, filtertype, this.map); + } else { + /** if layers already on the map - length not changed - update them */ + this.updateLayers(layers, mapLayers, filtertype); + // console.log(`update ${filtertype}`, layers, mapLayers); + } + + } + + private updateLayers(layers: ukisLayer[], mapLayerIds: string[], filtertype: Tgroupfiltertype) { + changeOrderOfLayers(this.map, layers, mapLayerIds, filtertype); + + for (const layer of layers) { + const mllayers = getAllLayers(this.map).filter(l => { + const ukismetadata = getUkisLayerMetadata(l as TypedStyleLayer) + return ukismetadata[UKIS_METADATA.layerID] === layer.id; + }).map(l => this.map.getLayer(l.id)).filter(l => l); + + mllayers.forEach(l => { + this.mapSvc.updateMlLayer(l as any, layer, this.map); + }) + } + } +} + + + diff --git a/projects/map-maplibre/src/lib/map-maplibre.module.ts b/projects/map-maplibre/src/lib/map-maplibre.module.ts new file mode 100644 index 000000000..d2ba88cc8 --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core'; +import { MapMaplibreComponent } from './map-maplibre.component'; +import { CommonModule } from '@angular/common'; +import { MapMaplibreService } from './map-maplibre.service'; + +@NgModule({ + declarations: [ + MapMaplibreComponent + ], + imports: [ + CommonModule + ], + exports: [ + MapMaplibreComponent + ], + providers: [MapMaplibreService] +}) +export class MapMaplibreModule { } diff --git a/projects/map-maplibre/src/lib/map-maplibre.service.spec.ts b/projects/map-maplibre/src/lib/map-maplibre.service.spec.ts new file mode 100644 index 000000000..66c25000e --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.service.spec.ts @@ -0,0 +1,294 @@ +import { TestBed } from '@angular/core/testing'; + +import { MapMaplibreService } from './map-maplibre.service'; +import { GeoJSONFeature, StyleSpecification, Map as glMap } from 'maplibre-gl'; +import { CustomLayer, RasterLayer, VectorLayer, WmsLayer, Layer as ukisLayer } from '@dlr-eoc/services-layers'; +import testFeatureCollection from '@dlr-eoc/shared-assets/geojson/testFeatureCollection.json'; +import { GeoJSONFeatureCollection } from 'ol/format/GeoJSON'; +import { createLayersFromGeojsonTypes } from './maplibre-layers.helpers'; + +const createMapTarget = (size: number[]) => { + const container = document.createElement('div'); + container.style.border = 'solid 1px #000'; + container.style.width = `${size[0]}px`; + container.style.height = `${size[1]}px`; + document.body.appendChild(container); + return { + size, + container + }; +}; + + +let ukisOsm: RasterLayer; +let ukisCustom: CustomLayer; +let ukisWms: WmsLayer; +let ukisGeoJson: VectorLayer; +const createLayers = () => { + ukisOsm = new RasterLayer({ + name: 'OpenStreetMap', + displayName: 'OpenStreetMap', + id: 'osm_2', + visible: false, + type: 'xyz', + url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + attribution: '©, OpenStreetMap contributors', + continuousWorld: false, + legendImg: 'https://a.tile.openstreetmap.org/3/4/3.png', + description: 'OpenStreetMap z-x-y Tiles', + opacity: 1 + }); + + ukisCustom = new CustomLayer({ + id: 'waterway-planet_eoc', + name: 'waterway', + visible: true, + removable: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + 'waterway-planet_eoc': // 'planet_eoc': + { + "type": "vector", + //@ts-ignore + "__Comment": "The url to the tilejson is not public available so we use the tiles array to skip the request, to make use of the tms service. See https://github.com/openlayers/ol-mapbox-style/blob/v8.2.1/src/util.js#L109", + "url": "", + "tiles": "abcd".split('').map(s => `s=>https://${s}.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true`) + } + }, + layers: [{ + "id": "waterway", + "type": "line", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(198, 100%, 28%)" + } + }, + { + "id": "water", + "type": "fill", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "hsl(198, 100%, 28%)" + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Medium Italic", + // "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "rgb(157,169,177)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + } + ] + } + }); + + + ukisWms = new WmsLayer({ + name: 'Sentinel-2 Europe', + id: 'sentinel2Europe', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'rgb', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://sgx.geodatenzentrum.de/wms_sen2europe', + attribution: '©, Europäische Union - BKG', + continuousWorld: false, + legendImg: 'https://sgx.geodatenzentrum.de/wms_sen2europe?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=rg', + opacity: 1 + }); + + ukisGeoJson = new VectorLayer({ + id: 'geojson_test', + name: 'GeoJSON Vector Layer', + attribution: `© DLR GeoJSON`, + type: 'geojson', + data: testFeatureCollection, + visible: false + }); +} + +describe('MapMaplibreService', () => { + let service: MapMaplibreService; + let map: glMap; + + + beforeEach(async () => { + TestBed.configureTestingModule({}); + service = TestBed.inject(MapMaplibreService); + createLayers(); + + const baseStyle: StyleSpecification = { + "version": 8, + "name": "Merged Style Specifications", + "metadata": { + }, + "sources": {}, + "sprite": "https://openmaptiles.github.io/positron-gl-style/sprite", + "glyphs": "http://fonts.openmaptiles.org/{fontstack}/{range}.pbf", + "layers": [] + }; + + const mapTarget = createMapTarget([1024, 768]); + map = new glMap({ + container: mapTarget.container, + style: baseStyle as StyleSpecification + }); + + // https://github.com/maplibre/maplibre-gl-js/discussions/2193 + await new Promise((resolve, reject) => { + map.once('idle', (evt) => { + resolve(evt); + }); + }); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + + it('should reset/add ukisLayers from a Type', () => { + const layers = [ukisCustom, ukisOsm, ukisGeoJson, ukisWms]; + const filtertype = 'Layers'; + service.setUkisLayers(layers, 'Layers', map); + const mapStyle = map.getStyle(); + expect(mapStyle.metadata[`ukis:${filtertype}IDs`]).toEqual(layers.map(b => b.id)); + }); + + it('should get all layers from a Type', () => { + const layers = [ukisCustom, ukisOsm, ukisGeoJson, ukisWms]; + const filtertype = 'Layers'; + service.setUkisLayers(layers, 'Layers', map); + + const addedLayers = service.getLayers(filtertype, map); + // ukisCustom layers.length and geojson layers for each type + const customLayers = ukisCustom.custom_layer.layers.map(l => l.id); + const geoJsonLayers = (ukisGeoJson.data as GeoJSONFeatureCollection).features.map((f: GeoJSONFeature, index: number) => createLayersFromGeojsonTypes(f, ukisGeoJson, index)).map(l => l.id); + expect(addedLayers.styleLayers.map(l => l.id)).toEqual([...customLayers, ukisOsm.id, ...geoJsonLayers, ukisWms.id]); + expect(addedLayers.styleLayers.length).toEqual((layers.length - 2) + customLayers.length + geoJsonLayers.length); + }); + + it('should get all layers for one ukis layer id', () => { + const layers = [ukisCustom, ukisOsm, ukisGeoJson, ukisWms]; + const filtertype = 'Layers'; + service.setUkisLayers(layers, 'Layers', map); + + const addedLayers = service.getLayersForId(ukisCustom.id, filtertype, map); + expect(addedLayers.styleLayers.length).toEqual(ukisCustom.custom_layer.layers.length); + }); + + + it('should update a TypedStyleLayer with updateMlLayer', () => { + const layers = [ukisGeoJson]; + const filtertype = 'Layers'; + service.setUkisLayers(layers, filtertype, map); + + const layerBeforUpdate = service.getLayersForId(ukisGeoJson.id, filtertype, map).styleLayers; + + ukisGeoJson.visible = true; + layerBeforUpdate.forEach(l => { + service.updateMlLayer(l as any, ukisGeoJson, map); + }); + + const layerAfterUpdate = service.getLayersForId(ukisGeoJson.id, filtertype, map).styleLayers; + layerAfterUpdate.forEach(l => { + expect(l.visibility).toBe("visible"); + }); + }); + + it('should update one ukisLayer from a Type - not reset', () => { + const layers = [ukisGeoJson]; + const filtertype = 'Layers'; + service.setUkisLayers(layers, filtertype, map); + + const layerBeforUpdate = service.getLayersForId(ukisGeoJson.id, filtertype, map).styleLayers; + layerBeforUpdate.forEach(l => { + expect(l.visibility).toBe("none"); + }); + + ukisGeoJson.visible = true; + service.setUkisLayers(layers, 'Layers', map); + const layerAfterUpdate = service.getLayersForId(ukisGeoJson.id, filtertype, map).styleLayers; + layerAfterUpdate.forEach(l => { + expect(l.visibility).toBe("visible"); + }); + }); + + + it('should add layers and sources from LayerSourceSpecification | StyleSpecification if they are not already on the map', () => { + const style: StyleSpecification = ukisCustom.custom_layer; + service.setLayers([style], map); + + const mapStyle = map.getStyle(); + expect(mapStyle.layers.length).toEqual(style.layers.length); + expect(Object.keys(mapStyle.sources).length).toEqual(Object.keys(style.sources).length); + }); + + + it('should only allow supported layers to be added', () => { + const newLayer = new ukisLayer({ + name: 'test layer', + id: 'test', + type: 'test' + }); + + // supported layers + // [XyzLayertype, WmsLayertype, WmtsLayertype, TmsLayertype, GeojsonLayertype, CustomLayertype, WfsLayertype, KmlLayertype, StackedLayertype]; + expect(service.layerIsSupported(newLayer)).toBe(false); + }); +}); diff --git a/projects/map-maplibre/src/lib/map-maplibre.service.ts b/projects/map-maplibre/src/lib/map-maplibre.service.ts new file mode 100644 index 000000000..3a947895f --- /dev/null +++ b/projects/map-maplibre/src/lib/map-maplibre.service.ts @@ -0,0 +1,234 @@ +import { Injectable } from '@angular/core'; +import { + Layer as ukisLayer, TFiltertypesUncap, TFiltertypes, +} from '@dlr-eoc/services-layers'; +import { Map as glMap, StyleSpecification, TypedStyleLayer } from 'maplibre-gl'; +import { BehaviorSubject } from 'rxjs'; +import { LayerSourceSpecification, UKIS_METADATA, setOpacity, setVisibility } from './maplibre.helpers'; +import { createLayer, layerIsSupported, updateSource } from './maplibre-layers.helpers'; + +type Tgroupfiltertype = TFiltertypesUncap | TFiltertypes; + +@Injectable({ + providedIn: 'root' +}) +export class MapMaplibreService { + + readonly FILTER_TYPE_KEY = 'filtertype' as const; + readonly ID_KEY = 'id' as const; + readonly TITLE_KEY = 'title' as const; + WebMercator = 'EPSG:3857'; + WGS84 = 'EPSG:4326'; + + public map = new BehaviorSubject(null); + constructor() { } + + /** + * This function resets/adds all layers in the StyleSpecification of a filtertype with the new UKIS-Layers + */ + public setUkisLayers(layers: Array, filtertype: Tgroupfiltertype, map: glMap) { + const lowerType = filtertype.toLowerCase() as Tgroupfiltertype; + const tempLayers: (LayerSourceSpecification | StyleSpecification)[] = []; + + if (layers.length < 1 && lowerType !== 'baselayers') { + // console.log('empty array set - remove layers of the type', layers); + // this.removeAllLayers(lowerType); + } else { + layers.forEach((newLayer) => { + const layerStyleSpec = this.createLayer(newLayer); + if (layerStyleSpec) { + + if ('version' in layerStyleSpec) { /** StyleSpecification */ + /** + * TODO: merge styles in current style + * - version: number + * - name: string + * - glyphs: string: + * - layers: Array<> + * - sources: Object<> + * - sprite ?: string | "An array of `{id: 'my-sprite', url: 'https://example.com/sprite'} + * - metadata ?: Object + * - id ?: string + */ + + const hasLayers = layerStyleSpec.layers.map(l => map.getLayer(l.id)).filter(l => l); + + // check if layer not undefined + if (!hasLayers.length) { + tempLayers.push(layerStyleSpec); + } + + // update layer if on map + if (hasLayers.length) { + hasLayers.forEach(l => this.updateMlLayer(l as any, newLayer, map)); + } + + // TODO: how to handle glyphs, sprite, terrain... + // const style = map.getStyle(); + // style.glyphs = layer.glyphs + // style.sprite = layer.sprite + // style.terrain = layer.terrain + + // TODO: check if sources are the same ?? - reuse + + } else if ('sources' in layerStyleSpec && 'layers' in layerStyleSpec) { /** LayerSourceSpecification */ + + const hasLayers = layerStyleSpec.layers.map(l => map.getLayer(l.id)).filter(l => l); + + // check if layer not undefined + if (!hasLayers.length) { + tempLayers.push(layerStyleSpec); + } + + // update layer if on map + if (hasLayers.length) { + hasLayers.forEach(l => this.updateMlLayer(l as any, newLayer, map)); + } + } + } + }); + } + + if (tempLayers.length > 0) { + this.setLayers(tempLayers, map); + const newTempLayer: { filtertype: Tgroupfiltertype, layers: (LayerSourceSpecification | StyleSpecification)[] } = { + filtertype: lowerType, layers: tempLayers + }; + map.style.stylesheet.metadata[`ukis:${filtertype}IDs`] = layers.map(l => l.id); + return newTempLayer; + } else { + return null; + } + } + + + /** + * Get all maplibre layers from the style with groupfiltertype + * + * see addUkisLayerMetadata + * + * @returns layerSpecifications and styleLayers + */ + public getLayers(filtertype: Tgroupfiltertype, map: glMap) { + const style = map.getStyle(); + const layerSpecifications = style.layers.filter(l => l.metadata[UKIS_METADATA.filtertype] === filtertype); + const styleLayers = layerSpecifications.map(ls => map.getLayer(ls.id)); + + return { + layerSpecifications, + styleLayers + } + } + + /** + * Get all maplibre layers from one ukis layer + * + * see addUkisLayerMetadata + * + * @param id id of the ukis layer + * @returns layerSpecifications and styleLayers + */ + public getLayersForId(id: string, filtertype: Tgroupfiltertype, map: glMap) { + const alllayers = this.getLayers(filtertype, map); + const layerSpecifications = alllayers.layerSpecifications.filter(l => l.metadata[UKIS_METADATA.layerID] === id); + if (layerSpecifications.length) { + const styleLayers = layerSpecifications.map(ls => map.getLayer(ls.id)); + return { + layerSpecifications, + styleLayers + }; + } else { + return null; + } + } + + + /** + * Add layers and sources from LayerSourceSpecification | StyleSpecification if they are not already on the map. + */ + public setLayers(layers: (LayerSourceSpecification | StyleSpecification)[], map: glMap) { + layers.forEach(layersAndSources => { + /* Check if StyleSpecification or LayerSourceSpecification + * We do not use map.setStyle because we want to merge all the styles. + * + * if ('version' in sl) { + * // StyleSpecification + * map.setStyle(sl); + * } else if ('sources' in sl && 'layers' in sl)... + */ + + if ('sources' in layersAndSources && 'layers' in layersAndSources) { /** StyleSpecification or LayerSourceSpecification */ + layersAndSources.layers.forEach(layerSpec => { + let sourceId: string; + if (layerSpec.type !== 'background') { + if (typeof layerSpec.source === 'object') { + // see - addLayer - https://github.com/maplibre/maplibre-gl-js/blob/HEAD/src/style/style.ts#L787-L788 + sourceId = layerSpec.id; + } else { + sourceId = layerSpec.source; + } + + const hasSource = map.getSource(sourceId); + if (!hasSource) { + const sorceDef = layersAndSources.sources[sourceId]; + if (sorceDef) { + map.addSource(sourceId, sorceDef); + } else { + console.log('Source was not found in the LayerSourceSpecification!') + } + } + + map.addLayer(layerSpec) + } else { + // background does not need a source + // https://maplibre.org/maplibre-style-spec/layers/ + map.addLayer(layerSpec) + } + }); + } + }); + + return layers; + } + + public updateMlLayer(mllayer: TypedStyleLayer, layer: ukisLayer, map: glMap) { + /** + * update ml layer + * - map.setLayoutProperty() > Visibility + * - map.setPaintProperty() > Opacity + * - map.moveLayer(layer.id, layerBeforeId) > index + * + * - map.setFilter() + * - map.setLayerZoomRange() + */ + // update visibility + // Set visibility only if it is not ignored in a custom layer. + const ignoreVisibility = mllayer.metadata['ukis:ignore-visibility']; + if (!ignoreVisibility) { + setVisibility(map, mllayer, layer.visible); + } + + + // update opacity + // Set opacity only if it is not ignored in a custom layer. + const ignoreOpacity = mllayer.metadata['ukis:ignore-opacity']; + if (!ignoreOpacity) { + setOpacity(map, mllayer, layer.opacity); + } + + this.updateLayerParamsAndSource(map, mllayer, layer) + } + + private updateLayerParamsAndSource(map: glMap, mllayer: TypedStyleLayer, layer: ukisLayer) { + if (layer.type === 'wms' || layer.type === 'wmts' || layer.type === 'tms' || layer.type === 'wfs' || layer.type === 'geojson') { + const oldSource = map.getSource(mllayer.source); + updateSource(map, layer, oldSource); + } + } + + public createLayer = createLayer; + public layerIsSupported = layerIsSupported; +} + + + diff --git a/projects/map-maplibre/src/lib/maplibre-layers.helpers.spec.ts b/projects/map-maplibre/src/lib/maplibre-layers.helpers.spec.ts new file mode 100644 index 000000000..e45a773a9 --- /dev/null +++ b/projects/map-maplibre/src/lib/maplibre-layers.helpers.spec.ts @@ -0,0 +1,317 @@ +import { CustomLayer, Layer, RasterLayer, VectorLayer, WmsLayer, WmtsLayer } from '@dlr-eoc/services-layers'; +import { + addUkisLayerMetadata, hasUkisLayerMetadata, getUkisLayerMetadata, createWmsLayer, createXyzLayer, + createWmtsLayer, createTmsLayer, createGeojsonLayer, createLayersFromGeojsonTypes, creteDefaultGeojsonLayers, createWfsLayer, createKmlLayer, createCustomLayer, + createStackedLayer, createGetMapUrl, createGetTileUrl, createBaseLayer +} from './maplibre-layers.helpers'; +import testFeatureCollection from '@dlr-eoc/shared-assets/geojson/testFeatureCollection.json'; +import { RasterSourceSpecification, StyleSpecification } from 'maplibre-gl'; +import { UKIS_METADATA, getOpacityPaintProperty } from './maplibre.helpers'; + + +let ukisOsm: RasterLayer; +let ukisCustom: CustomLayer; +let ukisWms: WmsLayer; +let ukisWmts: WmtsLayer; +let ukisGeoJson: VectorLayer; + +const createLayers = () => { + ukisOsm = new RasterLayer({ + name: 'OpenStreetMap', + displayName: 'OpenStreetMap', + id: 'osm_2', + visible: false, + type: 'xyz', + url: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + attribution: '©, OpenStreetMap contributors', + continuousWorld: false, + legendImg: 'https://a.tile.openstreetmap.org/3/4/3.png', + description: 'OpenStreetMap z-x-y Tiles', + opacity: 1 + }); + + ukisCustom = new CustomLayer({ + id: 'waterway-planet_eoc', + name: 'waterway', + visible: true, + removable: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + 'waterway-planet_eoc': // 'planet_eoc': + { + "type": "vector", + //@ts-ignore + "__Comment": "The url to the tilejson is not public available so we use the tiles array to skip the request, to make use of the tms service. See https://github.com/openlayers/ol-mapbox-style/blob/v8.2.1/src/util.js#L109", + "url": "", + "tiles": [ + "https://a.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://b.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://c.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://d.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true" + ] + } + }, + layers: [{ + "id": "waterway", + "type": "line", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(198, 100%, 28%)" + } + }, + { + "id": "water", + "type": "fill", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "hsl(198, 100%, 28%)" + } + }, + { + "id": "water_name", + "type": "symbol", + "source": "waterway-planet_eoc", // 'planet_eoc', + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Medium Italic", + // "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "rgb(157,169,177)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + } + ] + } + }); + + + ukisWms = new WmsLayer({ + name: 'Sentinel-2 Europe', + id: 'sentinel2Europe', + visible: false, + type: 'wms', + removable: false, + params: { + LAYERS: 'rgb', + FORMAT: 'image/png', + TRANSPARENT: true + }, + url: 'https://sgx.geodatenzentrum.de/wms_sen2europe', + attribution: '©, Europäische Union - BKG', + continuousWorld: false, + legendImg: 'https://sgx.geodatenzentrum.de/wms_sen2europe?service=WMS&version=1.3.0&request=GetLegendGraphic&format=image%2Fpng&width=20&height=20&layer=rg', + opacity: 1, + tileSize: 256 + }); + + ukisWmts = new WmtsLayer({ + name: 'EOC Litemap Tile', + displayName: 'EOC Litemap Tile', + id: 'eoc_litemap_tile', + visible: false, + type: 'wmts', + removable: false, + params: { + layer: 'eoc:litemap', + format: 'image/png', + style: '_empty', + matrixSetOptions: { + matrixSet: 'EPSG:3857', + tileMatrixPrefix: 'EPSG:3857' + } + }, + url: 'https://tiles.geoservice.dlr.de/service/wmts', + attribution: '©, DLR', + continuousWorld: false, + legendImg: 'https://tiles.geoservice.dlr.de/service/wmts?layer=eoc%3Alitemap&style=_empty&tilematrixset=EPSG%3A3857&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A3857%3A5&TileCol=18&TileRow=11', + description: 'EOC Litemap as web map tile service', + opacity: 1 + }); + + ukisGeoJson = new VectorLayer({ + id: 'geojson_test', + name: 'GeoJSON Vector Layer', + attribution: `© DLR GeoJSON`, + type: 'geojson', + data: testFeatureCollection, + visible: false + }); +} + +describe('MaplibreLayerHelpers', () => { + beforeEach(async () => { + createLayers(); + }); + + it('should create ukis Metadata for LayerSourceSpecification', () => { + const layer = new Layer({ + id: 'testlayer', + name: 'Test Layer', + type: 'custom', + filtertype: 'Layers', + }); + + const metadata = addUkisLayerMetadata(layer); + expect(metadata[UKIS_METADATA.filtertype]).toBe(layer.filtertype); + expect(metadata[UKIS_METADATA.layerID]).toBe(layer.id); + }); + + it('should create a base LayerSourceSpecification from ukis Rasterlayer', () => { + const ls = createBaseLayer(ukisOsm); + + const source = ls.source; + expect(source.type).toBe('raster'); + if (source.type === 'raster') { + expect(source.attribution).toBe(ukisOsm.attribution); + expect(source.tileSize).toBe(ukisOsm.tileSize || 256); + } + + const layer = ls.layer; + expect(layer.id).toBe(ukisOsm.id); + expect(layer.type).toBe('raster'); + if (layer.type === 'raster') { + expect(layer.source).toBe(ukisOsm.id); + // Type instantiation is excessively deep and possibly infinite + expect((layer.paint as any)['raster-opacity']).toBe(ukisOsm.opacity); + expect((layer.layout as any).visibility).toBe((ukisOsm.visible) ? 'visible' : 'none'); + expect(layer.metadata).toEqual(addUkisLayerMetadata(ukisOsm)); + expect(layer.minzoom).toBe(ukisOsm.minZoom); + expect(layer.maxzoom).toBe(ukisOsm.maxZoom); + } + }); + + it('should create LayerSourceSpecification from ukis WmsLayer', () => { + const ls = createWmsLayer(ukisWms); + const source = ls.sources[ukisWms.id]; + + if (source.type === 'raster') { + expect(source.tiles).toEqual([createGetMapUrl(ukisWms)]); + } + }); + + it('should create LayerSourceSpecification from ukis WmtsLayer', () => { + const ls = createWmtsLayer(ukisWmts); + const source = ls.sources[ukisWmts.id]; + + if (source.type === 'raster') { + expect(source.tiles).toEqual([createGetTileUrl(ukisWmts)]); + } + }); + + it('should create LayerSourceSpecification from ukis ukisGeoJson', () => { + const ls = createGeojsonLayer(ukisGeoJson); + const source = ls.sources[ukisGeoJson.id]; + + if (source.type === 'geojson') { + expect(source.data).toBe(ukisGeoJson.data); + } + }); + // TODO:createLayersFromGeojsonTypes + + // TODO:creteDefaultGeojsonLayers + + // TODO:createXyzLayer + + // TODO:createTmsLayer + + // TODO:createWfsLayer + + // TODO:createKmlLayer + + it('should create LayerSourceSpecification from ukis custom layer', () => { + const styleSpec = createCustomLayer(ukisCustom); + + const sources = styleSpec.sources; + Object.keys(sources).forEach(key => { + const s: any = sources[key]; + expect(s.attribution).toBe(ukisCustom.attribution); + }); + + + const layers = styleSpec.layers; + layers.forEach(ls => { + const metadata = addUkisLayerMetadata(ukisCustom); + expect(ls.metadata[UKIS_METADATA.filtertype]).toBe(metadata[UKIS_METADATA.filtertype]); + expect(ls.metadata[UKIS_METADATA.layerID]).toBe(metadata[UKIS_METADATA.layerID]); + expect(ls.id.split(':')[1]).toBe(ukisCustom.id); + expect(ls.layout.visibility).toBe((ukisCustom.visible) ? 'visible' : 'none'); + + const opacityPaintProperty = getOpacityPaintProperty(ls.type); + if (opacityPaintProperty) { + expect((ls.paint as any)[opacityPaintProperty]).toBe(ukisCustom.opacity); + } + }); + + }); + + it('should create LayerSourceSpecification from ukis custom layer but ignore some visibility', () => { + // set ignore-visibility + const customLayer_1 = ukisCustom.custom_layer.layers[1]; + customLayer_1.metadata = { + ['ukis:ignore-visibility']: true, + ['ukis:ignore-opacity']: true + }; + const styleSpec = createCustomLayer(ukisCustom); + + const layer_1 = styleSpec.layers[1]; + expect(layer_1.metadata['ukis:ignore-visibility']).toBe(true); + expect(layer_1.layout.visibility).toBe(customLayer_1.layout.visibility); + + const opacityPaintProperty = getOpacityPaintProperty(layer_1.type); + if (opacityPaintProperty) { + expect(layer_1.paint[opacityPaintProperty]).toBe(customLayer_1.paint[opacityPaintProperty]); + } + + // remove ignore-visibility + delete customLayer_1.metadata['ukis:ignore-visibility']; + delete customLayer_1.metadata['ukis:ignore-opacity']; + }); + + + // TODO:createStackedLayer +}); \ No newline at end of file diff --git a/projects/map-maplibre/src/lib/maplibre-layers.helpers.ts b/projects/map-maplibre/src/lib/maplibre-layers.helpers.ts new file mode 100644 index 000000000..0f42e1c33 --- /dev/null +++ b/projects/map-maplibre/src/lib/maplibre-layers.helpers.ts @@ -0,0 +1,602 @@ +import { + CircleLayerSpecification, FillLayerSpecification, GeoJSONFeature, GeoJSONSourceSpecification, LayerSpecification, Map as glMap, + LineLayerSpecification, RasterLayerSpecification, RasterSourceSpecification, SourceSpecification, StyleSpecification, SymbolLayerSpecification, TypedStyleLayer, VectorSourceSpecification, Source, GeoJSONSource +} from "maplibre-gl"; +import { + RasterLayer as ukisRasterLayer, WmsLayer as ukisWmsLayer, WmtsLayer as ukisWtmsLayer, + WmtsLayer as ukisWmtsLayer, VectorLayer as ukisVectorLayer, CustomLayer as ukisCustomLayer, Layer as ukisLayer, StackedLayer, XyzLayertype, WmsLayertype, WmtsLayertype, TmsLayertype, GeojsonLayertype, KmlLayertype, WfsLayertype, CustomLayertype, StackedLayertype +} from '@dlr-eoc/services-layers'; +import { LayerSourceSpecification, SourceIdSpecification, UKIS_METADATA, getAllLayers, getOpacityPaintProperty } from "./maplibre.helpers"; +import { propsEqual } from '@dlr-eoc/utilities'; + +export function addUkisLayerMetadata(l: ukisLayer) { + const metadata = {}; + metadata[UKIS_METADATA.filtertype] = l.filtertype; + metadata[UKIS_METADATA.layerID] = l.id; + return metadata; +} + +export function hasUkisLayerMetadata(ml: TypedStyleLayer) { + if ((ml?.metadata as any)[UKIS_METADATA.filtertype] || (ml?.metadata as any)[UKIS_METADATA.layerID]) { + return true; + } else { + return false; + } +} + +export function getUkisLayerMetadata(ml: TypedStyleLayer) { + const metadata = {}; + metadata[UKIS_METADATA.filtertype] = (ml?.metadata as any)[UKIS_METADATA.filtertype]; + metadata[UKIS_METADATA.layerID] = (ml?.metadata as any)[UKIS_METADATA.layerID]; + return metadata; +} + +export function createGetMapUrl(l: ukisWmsLayer) { + const baseurl = l.url; + const properties = l.params + let url = `${baseurl}?bbox={bbox-epsg-3857}&format=${properties?.FORMAT || 'image/png'}&service=WMS&version=${properties?.VERSION || '1.1.1'}&request=GetMap&srs=EPSG:3857&transparent=${properties?.TRANSPARENT || 'true'}&width=${l.tileSize || 256}&height=${l.tileSize || 256}&layers=${properties?.LAYERS}`; + if (properties.STYLES) { + url += `&styles=${properties.STYLES}` + } + return url; +} + +export function createGetTileUrl(l: ukisWtmsLayer) { + const baseurl = l.url; + const properties = l.params; + const matrix = 'EPSG:3857:{z}'; + + // https://github1s.com/openlayers/openlayers/blob/HEAD/src/ol/source/WMTS.js#L70-L71 + + // https://tiles.geoservice.dlr.de/service/wmts?layer=eoc%3Abasemap&style=_empty&tilematrixset=EPSG%3A3857&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A3857%3A5&TileCol=18&TileRow=11 + // bbox={bbox-epsg-3857}&ratio={ratio}&quadkey={quadkey}&z={z}&x={x}&y={y} + const url = `${baseurl}?layer=${properties?.layer}&style=${properties.style}&tilematrixset=${properties.matrixSetOptions?.matrixSet}&service=WTMS&version=${properties?.version || '1.0.0'}&request=GetTile&TileMatrix=${matrix}&TileCol={x}&TileRow={y}&format=${properties?.format || 'image/png'}`; + return url; +} + +function returnSourcesAndLayers(l: ukisLayer, source: SourceSpecification | KmlSourceSpecification, layers: LayerSpecification[]) { + const sources: SourceIdSpecification = {}; + sources[l.id] = source as SourceSpecification; + + return { + sources, + layers + } as LayerSourceSpecification; +} + + +export function layerIsSupported(layer: ukisLayer) { + const supportedLayers = [XyzLayertype, WmsLayertype, WmtsLayertype, TmsLayertype, GeojsonLayertype, CustomLayertype, WfsLayertype, KmlLayertype, StackedLayertype]; + const supported = supportedLayers.includes(layer.type); + if (!supported) { + console.warn(`layer of type ${layer.type} is not supported!`) + } + return supported; +} + +/** + * This function is used as the basis for all layers. + * Wms | Xyz | Wmts | Geojson | Wfs | Kml + */ +export function createBaseLayer(l: ukisRasterLayer | ukisVectorLayer) { + const source: VectorSourceSpecification | RasterSourceSpecification | GeoJSONSourceSpecification | KmlSourceSpecification = {} as any; + const layer: FillLayerSpecification | LineLayerSpecification | SymbolLayerSpecification | CircleLayerSpecification | RasterLayerSpecification = {} as any; + + if (l instanceof ukisVectorLayer) { + source.type = 'vector'; + + if (l.type === 'geojson' || l.type === 'wfs') { + source.type = 'geojson'; + (source as GeoJSONSourceSpecification).data = l.data || { type: 'FeatureCollection', features: [] }; + } else if (l.type === 'kml') { + source.type = 'kml'; + (source as KmlSourceSpecification).data = l.data || { type: 'FeatureCollection', features: [] }; + } + + if (source.type === 'kml' || source.type === 'geojson') { + if (l.data) { + source.data = l.data; + } else if (l.url) { + source.data = l.url; + } + + if (l.cluster) { + source.cluster = true; + if (typeof l.cluster !== 'boolean') { + if (l.cluster?.clusterRadius || l.cluster.distance) source.clusterRadius = l.cluster.clusterRadius | l.cluster.distance; + if (l.cluster?.clusterMaxZoom) source.clusterMaxZoom = l.cluster.clusterMaxZoom; + if (l.cluster?.clusterMinPoints) source.clusterMinPoints = l.cluster.clusterMinPoints; + if (l.cluster?.clusterProperties) source.clusterProperties = l.cluster.clusterProperties; + } + } + } + + if (l.bbox && source.type === 'vector') { + source.bounds = l.bbox as any; + } + + } else if (l instanceof ukisRasterLayer) { + source.type = 'raster'; + if (l.bbox && source.type === 'raster') { + source.bounds = l.bbox as any; + } + } + + if (l.attribution) { + source.attribution = l.attribution; + } + + if (source.type === 'raster' && l instanceof ukisRasterLayer) { + if (l.tileSize) { source.tileSize = l.tileSize; } + else { source.tileSize = 256; } + } + + layer.id = l.id; + layer.type = 'raster'; + layer.source = l.id; + layer.paint = { + 'raster-opacity': l.opacity + }; + layer.layout = { + visibility: (l.visible) ? 'visible' : 'none' + }; + layer.metadata = addUkisLayerMetadata(l); + + if (l.maxZoom || l.maxZoom === 0) { layer.maxzoom = l.maxZoom; } + if (l.minZoom || l.minZoom === 0) { layer.minzoom = l.minZoom; } + + return { + source: source as T, + layer + } +} + +export function createWmsLayer(l: ukisWmsLayer) { + const { source, layer } = createBaseLayer(l); + source.tiles = (l.subdomains) ? l.subdomains.map(s => { + l.url = l.url.replace('{s}', s); + return createGetMapUrl(l); + }) : [createGetMapUrl(l)]; + return returnSourcesAndLayers(l, source, [layer]); +} + +export function createXyzLayer(l: ukisRasterLayer) { + const { source, layer } = createBaseLayer(l); + source.tiles = (l.subdomains) ? l.subdomains.map(s => l.url.replace('{s}', s)) : [l.url]; + source.scheme = 'xyz'; + return returnSourcesAndLayers(l, source, [layer]); +} + +export function createWmtsLayer(l: ukisWtmsLayer) { + const { source, layer } = createBaseLayer(l); + source.tiles = [createGetTileUrl(l)]; + return returnSourcesAndLayers(l, source, [layer]); +} + + +type tmsReturnType = T extends ukisRasterLayer ? LayerSourceSpecification : + T extends ukisVectorLayer ? StyleSpecification : never; + +export function createTmsLayer(l: T): tmsReturnType { + let layerSourceOrStyleSpecification: any; + if (l instanceof ukisRasterLayer) { + const sl = createXyzLayer(l); + + layerSourceOrStyleSpecification = sl as LayerSourceSpecification; + + } else if (l instanceof ukisVectorLayer) { + const style = l?.options?.style as StyleSpecification; + style.layers.forEach(ls => { + (ls.metadata as any) = Object.assign(ls.metadata as any || {}, addUkisLayerMetadata(l)); + + // Set not visible on start + // TODO: ??? + if (!ls.layout) { + ls.layout = { + visibility: 'none' + } + } else { + ls.layout.visibility = 'none'; + } + }); + layerSourceOrStyleSpecification = style as StyleSpecification; + // TODO: merge styles??? + } + return layerSourceOrStyleSpecification; +} + + +export function createGeojsonLayer(l: ukisVectorLayer) { + const { source } = createBaseLayer(l) + let layers: LayerSpecification[] = []; + if (typeof l.data === 'object') { + if (l.data.type === 'Feature') { + layers = [createLayersFromGeojsonTypes(l.data, l)]; + } else if (l.data.type === 'FeatureCollection') { + if (!l.data || !l.data.features.length) { + layers = creteDefaultGeojsonLayers(l); + } else { + layers = l.data.features.map((f: GeoJSONFeature, index: number) => createLayersFromGeojsonTypes(f, l, index)); + } + } + } else { + // url data + const defaultGeom = [ + { + type: 'Feature', + geometry: { + type: 'Polygon' + } + }, + { + type: 'Feature', + geometry: { + type: 'LineString' + } + }, + { + type: 'Feature', + geometry: { + type: 'Point' + } + } + ] + layers = defaultGeom.map((f: any) => createLayersFromGeojsonTypes(f, l)); + } + + return returnSourcesAndLayers(l, source, layers); +} + +export function createLayersFromGeojsonTypes(feature: GeoJSONFeature, l: ukisLayer, index?: number) { + let layer: LayerSpecification = {} as never; + const style = { + fill: { + color: feature?.properties?.fill || 'rgba(255,255,255,0.4)', + }, + stroke: { + color: feature?.properties?.stroke || '#3399CC', + width: 1.25, + }, + circle: { + radius: 5 + } + }; + + switch (feature.geometry.type) { + case 'Polygon': + layer = { + id: `${l.id}:fill`, + type: 'fill', + source: l.id, + paint: { + 'fill-opacity': l.opacity, + 'fill-color': style.fill.color, + }, + layout: { + visibility: (l.visible) ? 'visible' : 'none' + }, + metadata: {}, + filter: ['==', '$type', 'Polygon'] + }; + layer.metadata[UKIS_METADATA.filtertype] = l.filtertype; + layer.metadata[UKIS_METADATA.layerID] = l.id; + break; + case 'LineString': + layer = { + id: `${l.id}:line`, + type: 'line', + source: l.id, + paint: { + 'line-opacity': l.opacity, + 'line-color': style.stroke.color, + 'line-width': style.stroke.width + }, + layout: { + 'line-join': 'round', + 'line-cap': 'round', + visibility: (l.visible) ? 'visible' : 'none' + }, + metadata: {}, + filter: ['in', '$type', 'LineString', 'Polygon'] + }; + layer.metadata[UKIS_METADATA.filtertype] = l.filtertype; + layer.metadata[UKIS_METADATA.layerID] = l.id; + break; + case 'Point': + layer = { + id: `${l.id}:circle`, + type: 'circle', + source: l.id, + paint: { + 'circle-opacity': l.opacity, + 'circle-stroke-opacity': l.opacity, + 'circle-stroke-color': style.stroke.color, + 'circle-color': style.fill.color, + 'circle-radius': style.circle.radius, + 'circle-stroke-width': style.stroke.width, + }, + layout: { + visibility: (l.visible) ? 'visible' : 'none' + }, + metadata: {}, + filter: ['==', '$type', 'Point'] + }; + layer.metadata[UKIS_METADATA.filtertype] = l.filtertype; + layer.metadata[UKIS_METADATA.layerID] = l.id; + break; + } + + if (typeof index === 'number') { + layer.id += `:${index}`; + } + + if (l.maxZoom || l.maxZoom === 0) layer.maxzoom = l.maxZoom; + if (l.minZoom || l.minZoom === 0) layer.minzoom = l.minZoom; + + return layer; +} + +export function creteDefaultGeojsonLayers(l: ukisVectorLayer) { + const fill = 'rgba(255,255,255,0.4)'; + const stroke = '#3399CC'; + const defaultGeom: Omit[] = [ + { + type: 'Feature', + geometry: { + type: 'Polygon', + coordinates: [] as any + }, + properties: { + fill, + stroke + } + }, + { + type: 'Feature', + geometry: { + type: 'LineString', + coordinates: [] as any + }, + properties: { + fill, + stroke + } + }, + { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [] as any + }, + properties: { + fill, + stroke + } + } + ] + return defaultGeom.map((f: GeoJSONFeature, index: number) => createLayersFromGeojsonTypes(f, l, index)); +} + +/** + * This could be improved by something like + * - https://github.com/maplibre/maplibre-gl-js/discussions/1078 -> addProtocol + * - https://openlayers.org/en/latest/apidoc/module-ol_source_Vector.html#~LoadingStrategy + * - https://openlayers.org/en/latest/apidoc/module-ol_source_TileWMS-TileWMS.html + * Or force the server to provide vector tiles :D -> e.g. https://docs.geoserver.org/main/en/user/extensions/vectortiles/tutorial.html + */ +export function createWfsLayer(l: ukisVectorLayer) { + let url = null; + if (l.url) { + if (l.url.indexOf('http://') === 0 || l.url.indexOf('https://') === 0) { + url = new URL(l.url); + } else { + url = new URL(l.url, window.location.origin); + } + + // making sure that srsname is set to projection for GeoJson + url.searchParams.set('srsname', 'EPSG:4326'); + // url.searchParams.set('bbox', `{bbox-epsg-3857},EPSG:3857`); + + l.url = url.toString(); + } + + return createGeojsonLayer(l); +} + + +export type KmlSourceSpecification = Omit & { type: "kml" }; +export function createKmlLayer(l: ukisVectorLayer) { + /** + * use map.addSourceType('kml', KMLSource,...) + * and extend the geojson source to convert kml to geojson and then use it. + * see -> map-maplibre.component.ts + */ + const { sources, layers } = createGeojsonLayer(l); + const source: KmlSourceSpecification = sources[l.id] as never; + source.type = 'kml'; + return returnSourcesAndLayers(l, source, layers); +} + + +export function createCustomLayer(l: ukisCustomLayer) { + + const isStyleSpec = l.custom_layer?.version && l.custom_layer?.sources && l.custom_layer?.layers; + if (!isStyleSpec) { + console.error('custom_layer is not a StyleSpecification'); + } + + const style = l.custom_layer as StyleSpecification; + + const sources: SourceIdSpecification = style.sources; + Object.keys(sources).forEach(key => { + const s = sources[key]; + if (!(s as any).attribution && l.attribution) { + (s as any).attribution = l.attribution; + } + }); + + + const layers: LayerSpecification[] = style.layers; + layers.forEach(ls => { + ls.id = `${ls.id}:${l.id}`; + ls.metadata = Object.assign(ls.metadata as any || {}, addUkisLayerMetadata(l)); + + // Set visibility only if it is not ignored in a custom layer. + // Allow hidden or always visible layers in a custom layer. + const ignoreVisibility = ls.metadata?.['ukis:ignore-visibility'] + if (!ignoreVisibility) { + if (!ls.layout) { + ls.layout = {}; + } + ls.layout.visibility = (l.visible) ? 'visible' : 'none'; + } + + const opacityPaintProperty = (ls.paint) ? getOpacityPaintProperty(ls.type) : null; + // Set the opacity only if it is not ignored in a custom layer. + const ignoreOpacity = ls.metadata?.['ukis:ignore-opacity'] + if (opacityPaintProperty && !ignoreOpacity) { + if (!ls.paint) { + ls.paint = {}; + } + (ls.paint as any)[opacityPaintProperty] = l.opacity; + } + }); + + + return style; +} + + +export function createStackedLayer(l: StackedLayer) { + if (l instanceof StackedLayer) { + const layersStyles = l.layers.map(ml => { + // Set visibility and opacity from the StackedLayer as start for all the layers + // they will be updated later in map-component + ml.visible = l.visible; + ml.opacity = l.opacity; + + /** popups are get from the olLayer later so add them */ + /* if (l.popup) { + ml.popup = l.popup; + } */ + + /** events are get from the olLayer later so add them */ + /* if (l.events) { + ml.events = l.events; + } */ + + /** Only crete layers that are not stacked. */ + if (ml instanceof StackedLayer !== true) { + return createLayer(ml); + } + }); + + const sources: SourceIdSpecification | StyleSpecification['sources'] = {}; + const layers: LayerSpecification[] = []; + // This has to be done like for a custom layer, so only one mlLayer exists for the stack. + layersStyles.forEach(lsGroup => { + lsGroup.layers.forEach(ls => { + ls.id = `${ls.id}:${l.id}`; + ls.metadata = Object.assign(ls.metadata as any || {}, addUkisLayerMetadata(l)); + layers.push(ls); + }); + + Object.keys(lsGroup.sources).forEach(s => { + sources[s] = lsGroup.sources[s]; + }); + }); + + return { + sources, + layers + } as LayerSourceSpecification; + } else { + console.log('layer is not of type StackedLayer!', l); + } +} + +/** + * all layers + */ + +export function createLayer(newLayer: ukisLayer) { + let newLlayer: (LayerSourceSpecification | StyleSpecification | undefined); + switch (newLayer.type) { + case XyzLayertype: + newLlayer = createXyzLayer(newLayer as ukisRasterLayer); + break; + case WmsLayertype: + newLlayer = createWmsLayer(newLayer as ukisWmsLayer); + break; + case WmtsLayertype: + newLlayer = createWmtsLayer(newLayer as ukisWmtsLayer); + break; + case TmsLayertype: + newLlayer = createTmsLayer(newLayer as ukisVectorLayer | ukisRasterLayer); + break; + case GeojsonLayertype: + newLlayer = createGeojsonLayer(newLayer as ukisVectorLayer); + break; + case KmlLayertype: + newLlayer = createKmlLayer(newLayer as ukisVectorLayer); + break; + case WfsLayertype: + newLlayer = createWfsLayer(newLayer as ukisVectorLayer); + break; + case CustomLayertype: + newLlayer = createCustomLayer(newLayer as ukisCustomLayer); + break; + case StackedLayertype: + newLlayer = createStackedLayer(newLayer as StackedLayer); + break; + } + return newLlayer; +} + + +export function updateSource(map: glMap, layer: ukisLayer, oldSource: Source) { + /* if (oldSource.type === 'geojson' && oldSource instanceof GeoJSONSource) { + if (layer.type === 'geojson' && layer instanceof ukisVectorLayer) { + if (typeof layer.cluster === 'object') { + oldSource.setClusterOptions(layer.cluster) + } + + oldSource.setData(layer.data); + return; + } + } */ + + + /* if(oldSource.type === 'image'){ + oldSource.updateImage() + } */ + const oldSourceSpec = map.getStyle().sources[layer.id]; + if (oldSourceSpec) { + const allLayers = getAllLayers(map); + const layersWhitSource = allLayers.filter(l => { + if (l.type !== 'background') { + return l.source === layer.id; + } + }); + const newLS = createLayer(layer); + const newSourceSpec = newLS.sources[layer.id]; + + const diff = !propsEqual(newSourceSpec, oldSourceSpec); + if (diff) { + layersWhitSource.forEach(l => { + map.removeLayer(l.id); + }); + map.removeSource(layer.id); + + console.log('update source', newSourceSpec); + map.addSource(layer.id, newSourceSpec); + layersWhitSource.forEach(l => { + map.addLayer(l); + console.log('update layer for source', l.id); + }); + } + } +} + + + diff --git a/projects/map-maplibre/src/lib/maplibre.helpers.spec.ts b/projects/map-maplibre/src/lib/maplibre.helpers.spec.ts new file mode 100644 index 000000000..8119d6b59 --- /dev/null +++ b/projects/map-maplibre/src/lib/maplibre.helpers.spec.ts @@ -0,0 +1,522 @@ +import { getOpacity, setOpacity, setVisibility, getAllLayers, getUkisLayerIDs, getLayersAndSources, removeLayerAndSource, changeOrderOfLayers, LayerSourceSpecification } from './maplibre.helpers'; +import { StyleSpecification, LayerSpecification, SourceSpecification, Map as glMap } from 'maplibre-gl'; +import { CustomLayer } from '@dlr-eoc/services-layers'; +import { addUkisLayerMetadata } from './maplibre-layers.helpers'; + +const createMapTarget = (size: number[]) => { + const container = document.createElement('div'); + container.style.border = 'solid 1px #000'; + container.style.width = `${size[0]}px`; + container.style.height = `${size[1]}px`; + document.body.appendChild(container); + return { + size, + container + }; +}; + +let ukisWaterLayer: CustomLayer; +let ukisLandLayer: CustomLayer; +let ukisPlaceLayer: CustomLayer; + +let planet_eoc: SourceSpecification; +let waterLayer: LayerSpecification; +let waterwayLayer: LayerSpecification; +let waterSymbolLayer: LayerSpecification; + +let landcoverLayer: LayerSpecification; +let landuseLayer: LayerSpecification; + +let placeVillage: LayerSpecification; +let placeTown: LayerSpecification; +let placeCity: LayerSpecification; + +const ukisWaterID = 'water-group'; +const ukisLandID = 'land-group'; +const ukisPlaceID = 'place-group'; + +const createLayers = () => { + planet_eoc = { + "type": "vector", + //@ts-ignore + "__Comment": "The url to the tilejson is not public available so we use the tiles array to skip the request, to make use of the tms service. See https://github.com/openlayers/ol-mapbox-style/blob/v8.2.1/src/util.js#L109", + "url": "", + "tiles": [ + "https://a.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://b.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://c.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true", + "https://d.tiles.geoservice.dlr.de/service/tms/1.0.0/planet_eoc@EPSG%3A900913@pbf/{z}/{x}/{y}.pbf?flipy=true" + ] + }; + + waterLayer = { + "id": "water", + "type": "fill", + "source": "planet_eoc", + "source-layer": "water", + "filter": [ + "all", + [ + "==", + "$type", + "Polygon" + ], + [ + "!=", + "brunnel", + "tunnel" + ] + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "fill-antialias": true, + "fill-color": "hsl(198, 100%, 28%)", + "fill-opacity": 1 + } + }; + + waterwayLayer = { + "id": "waterway", + "type": "line", + "source": 'planet_eoc', + "source-layer": "waterway", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "visibility": "visible" + }, + "paint": { + "line-color": "hsl(198, 100%, 28%)" + } + } + + waterSymbolLayer = { + "id": "water_name", + "type": "symbol", + "source": 'planet_eoc', + "source-layer": "water_name", + "filter": [ + "==", + "$type", + "LineString" + ], + "layout": { + "symbol-placement": "line", + "symbol-spacing": 500, + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Medium Italic", + // "Noto Sans Italic" + ], + "text-rotation-alignment": "map", + "text-size": 12 + }, + "paint": { + "text-color": "rgb(157,169,177)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + } + + landcoverLayer = { + "id": "landcover_wood", + "type": "fill", + "source": 'planet_eoc', + "source-layer": "landcover", + "minzoom": 10, + "filter": [ + "all", + ["==", "$type", "Polygon"], + ["==", "class", "wood"] + ], + "layout": { "visibility": "visible" }, + "paint": { + "fill-color": "rgba(106, 97, 68, 1)", + "fill-opacity": 1 + } + } + + landuseLayer = { + "id": "landuse_residential", + "type": "fill", + "source": 'planet_eoc', + "source-layer": "landuse", + "maxzoom": 16, + "filter": [ + "all", + ["==", "$type", "Polygon"], + ["==", "class", "residential"] + ], + "layout": { "visibility": "visible" }, + "paint": { + "fill-color": "rgba(135, 135, 49, 1)", + "fill-opacity": 0.9 + } + } + + placeVillage = { + "id": "place_village", + "type": "symbol", + "source": "planet_eoc", + "source-layer": "place", + "minzoom": 11, + "maxzoom": 24, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "village" + ] + ], + "layout": { + "icon-size": 0.4, + "text-anchor": "left", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(117, 129, 145)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + }; + placeTown = { + "id": "place_town", + "type": "symbol", + "source": "planet_eoc", + "source-layer": "place", + "minzoom": 9, + "maxzoom": 15, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "==", + "class", + "town" + ] + ], + "layout": { + "icon-image": "circle-11", + "icon-size": 0.4, + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(117, 129, 145)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + }; + placeCity = { + "id": "place_city", + "type": "symbol", + "source": "planet_eoc", + "source-layer": "place", + "minzoom": 7, + "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "$type", + "Point" + ], + [ + "all", + [ + "!=", + "capital", + 2 + ], + [ + "==", + "class", + "city" + ], + [ + ">", + "rank", + 3 + ] + ] + ], + "layout": { + "icon-image": "circle-11", + "icon-size": 0.4, + "text-anchor": "center", + "text-field": "{name:latin}\n{name:nonlatin}", + "text-font": [ + "Metropolis Regular", + "Noto Sans Regular" + ], + "text-justify": "left", + "text-offset": [ + 0.5, + 0.2 + ], + "text-size": 10, + "text-transform": "uppercase", + "visibility": "visible" + }, + "paint": { + "icon-opacity": 0.7, + "text-color": "rgb(117, 129, 145)", + "text-halo-blur": 1, + "text-halo-color": "rgb(242,243,240)", + "text-halo-width": 1 + } + }; + + ukisWaterLayer = new CustomLayer({ + id: ukisWaterID, + name: ukisWaterID, + filtertype: 'Layers', + visible: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + planet_eoc: planet_eoc + }, + layers: [waterLayer, waterwayLayer, waterSymbolLayer] + } + }); + ukisWaterLayer.custom_layer.layers.forEach(l => { + l.metadata = addUkisLayerMetadata(ukisWaterLayer); + }); + + ukisLandLayer = new CustomLayer({ + id: ukisLandID, + name: ukisLandID, + filtertype: 'Layers', + visible: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + planet_eoc: planet_eoc + }, + layers: [landcoverLayer, landuseLayer] + } + }); + ukisLandLayer.custom_layer.layers.forEach(l => { + l.metadata = addUkisLayerMetadata(ukisLandLayer); + }) + + ukisPlaceLayer = new CustomLayer({ + id: ukisPlaceID, + name: ukisPlaceID, + filtertype: 'Overlays', + visible: true, + custom_layer: { + version: 8, + // Use a different source for layers, to improve render quality + sources: { + planet_eoc: planet_eoc + }, + layers: [placeVillage, placeTown, placeCity] + } + }); + ukisPlaceLayer.custom_layer.layers.forEach(l => { + l.metadata = addUkisLayerMetadata(ukisPlaceLayer); + }); + + +} + + +describe('MaplibreHelpers', () => { + let map: glMap; + + beforeEach(async () => { + createLayers(); + + const baseStyle: StyleSpecification = { + "version": 8, + "name": "Merged Style Specifications", + "metadata": { + }, + "sources": {}, + "sprite": "https://openmaptiles.github.io/positron-gl-style/sprite", + "glyphs": "http://fonts.openmaptiles.org/{fontstack}/{range}.pbf", + "layers": [] + }; + + const mapTarget = createMapTarget([1024, 768]); + map = new glMap({ + container: mapTarget.container, + style: baseStyle as StyleSpecification + }); + + // https://github.com/maplibre/maplibre-gl-js/discussions/2193 + await new Promise((resolve, reject) => { + map.once('idle', (evt) => { + resolve(evt); + }); + }); + }); + + it('should set Layout Property visibility of a layer', () => { + map.addSource('planet_eoc', planet_eoc); + map.addLayer(waterLayer); + + const mapLayer = map.getLayer(waterLayer.id); + expect(mapLayer.visibility).toBe('visible'); + setVisibility(map, waterLayer.id, false); + + const newMapLayer = map.getLayer(waterLayer.id); + expect(newMapLayer.visibility).toBe('none'); + }); + + + it('should set/get Paint Property opacity of a layer', () => { + map.addSource('planet_eoc', planet_eoc); + map.addLayer(waterLayer); + + const opacity = 0.6; + expect(getOpacity(map, waterLayer.id)).toBe(1); + setOpacity(map, waterLayer.id, opacity); + + expect(getOpacity(map, waterLayer.id)).toBe(opacity); + }); + + it('should get all Layers from the style with a filtertype', () => { + map.addSource('planet_eoc', planet_eoc); + const layers = [...ukisWaterLayer.custom_layer.layers, ...ukisLandLayer.custom_layer.layers]; + layers.forEach(l => { + map.addLayer(l); + }); + + ukisPlaceLayer.custom_layer.layers.forEach(l => { + map.addLayer(l); + }); + + const maplayers = getAllLayers(map, 'Layers'); + expect(maplayers.length).toBe(layers.length); + expect(maplayers.map(l => l.id)).toEqual(layers.map(l => l.id)); + }); + + + it('should get all LayerGroups from the style', () => { + map.addSource('planet_eoc', planet_eoc); + const layers = [...ukisWaterLayer.custom_layer.layers, ...ukisLandLayer.custom_layer.layers]; + layers.forEach(l => { + map.addLayer(l); + }); + + ukisPlaceLayer.custom_layer.layers.forEach(l => { + map.addLayer(l); + }); + + const layerGroups = getUkisLayerIDs(map); + expect(layerGroups.length).toBe(3); + expect(layerGroups[0]).toBe(ukisWaterID); + expect(layerGroups[1]).toBe(ukisLandID); + expect(layerGroups[2]).toBe(ukisPlaceID); + }); + + + it('should get layers and sources for ukis:layerID', () => { + map.addSource('planet_eoc', planet_eoc); + const layers = [...ukisWaterLayer.custom_layer.layers, ...ukisLandLayer.custom_layer.layers]; + layers.forEach(l => { + map.addLayer(l); + }); + + ukisPlaceLayer.custom_layer.layers.forEach(l => { + map.addLayer(l); + }); + + const layerSources = getLayersAndSources(map, ukisWaterID); + expect(layerSources.layers).toEqual(ukisWaterLayer.custom_layer.layers); + expect(layerSources.sources['planet_eoc']).toEqual(planet_eoc); + }); + + it('should remove a layer and source for ukis:layerID', () => { + map.addSource('planet_eoc', planet_eoc); + map.addLayer({ + "id": "background", + "type": "background", + "paint": { + "background-color": "rgb(242,243,240)" + } + }); + + const layers = [...ukisWaterLayer.custom_layer.layers, ...ukisLandLayer.custom_layer.layers]; + layers.forEach(l => { + map.addLayer(l); + }); + + ukisPlaceLayer.custom_layer.layers.forEach(l => { + map.addLayer(l); + }); + + // only remove source if not used by other layer !!! + removeLayerAndSource(map, ukisWaterID); + const maplayers = getAllLayers(map); + expect(maplayers.length).toBe(1 + ukisLandLayer.custom_layer.layers.length + ukisPlaceLayer.custom_layer.layers.length); + expect(map.getSource('planet_eoc')).toBeDefined(); + }); + + it('should change order of map layers', () => { + map.addSource('planet_eoc', planet_eoc); + const layers = [...ukisWaterLayer.custom_layer.layers, ...ukisLandLayer.custom_layer.layers]; + layers.forEach(l => { + map.addLayer(l); + }); + + const ukisMapLayers = getUkisLayerIDs(map, 'Layers'); + + const changeLayers = [ukisLandLayer, ukisWaterLayer]; + const allChangeLayers = changeLayers.map(l => l.custom_layer.layers).flat(1).map(l => l.id); + + changeOrderOfLayers(map, changeLayers, ukisMapLayers, 'Layers'); + const newMapLayers = getAllLayers(map, 'Layers').map(l => l.id); + expect(newMapLayers).toEqual(allChangeLayers); + }); + +}); diff --git a/projects/map-maplibre/src/lib/maplibre.helpers.ts b/projects/map-maplibre/src/lib/maplibre.helpers.ts new file mode 100644 index 000000000..4b35a0dd2 --- /dev/null +++ b/projects/map-maplibre/src/lib/maplibre.helpers.ts @@ -0,0 +1,368 @@ + +import { Map as glMap, LngLatBounds, LngLat, LayerSpecification, TypedStyleLayer, SourceSpecification } from 'maplibre-gl'; +import { TGeoExtent, Layer as ukisLayer, TFiltertypes, TFiltertypesUncap } from '@dlr-eoc/services-layers'; + +/** Layers can consist of multiple layers and sources, e.g. if they are a VectorTileLayer - StyleSpecification */ +export type SourceIdSpecification = { [id: string]: SourceSpecification }; +export type LayerSourceSpecification = { sources: SourceIdSpecification, layers: LayerSpecification[] }; + +type Tgroupfiltertype = TFiltertypesUncap | TFiltertypes; + +export const UKIS_METADATA = { + layerID: 'ukis:layerID', + filtertype: 'ukis:filtertype' +}; + + +export function setExtent(map: glMap, extent: TGeoExtent, geographic?: boolean, fitOptions?: any): TGeoExtent { + const bounds = new LngLatBounds([extent[0], extent[1]], [extent[2], extent[3]]); + map.fitBounds(bounds); + + // TODO: wait before return ? + return getExtent(map, geographic); +} + +export function getExtent(map: glMap, geographic?: boolean): TGeoExtent { + const newbounds = map.getBounds(); + const newExtent = [newbounds.getSouth(), newbounds.getWest(), newbounds.getNorth(), newbounds.getEast()] as TGeoExtent; + return newExtent; +} + + +export function setCenter(map: glMap, center: number[], geographic?: boolean): number[] { + const lngLat = new LngLat(center[0], center[1]); + map.setCenter(lngLat); + + + // TODO: wait before return ? + return getCenter(map, geographic); +} + +export function getCenter(map: glMap, geographic?: boolean): number[] { + const newLngLat = map.getCenter(); + return [newLngLat.lng, newLngLat.lat]; +} + + +export function setZoom(map: glMap, zoom: number, notifier?: 'map' | 'user') { + map.setZoom(zoom); +} + +export function getZoom(map: glMap, notifier?: 'map' | 'user') { + return map.getZoom(); +} + + +export function setVisibility(map: glMap, layerOrId: string | TypedStyleLayer, visibility: boolean, cb?: () => void) { + let mllayer; + if (typeof layerOrId === 'string') { + mllayer = map.getLayer(layerOrId) as TypedStyleLayer | undefined; + } else { + mllayer = layerOrId; + } + if (mllayer && (mllayer.visibility === 'visible') !== visibility) { + // On custom layers, only the group is set, not the layers, so they can be controlled by the user + // layerOrGroupSetVisible(mllayer, layer.visible, layer instanceof CustomLayer); + map.setLayoutProperty(mllayer.id, 'visibility', (visibility) ? 'visible' : 'none'); + + // fixes https://github.com/dlr-eoc/ukis-frontend-libraries/issues/120 + // When a layer is set hidden, it's associated popups get a hidden class. + /* this.mapSvc.hideAllPopups(!layer.visible, (item) => { + // only hide the popups from the current layer + const elementID = item.getId(); + const layerID = elementID.toString().split(':')[0]; + if (layerID) { + if (layerID === layer.id) { + return layerID === layer.id; + } + } else { + return true; + } + }); */ + } +} + +export function setOpacity(map: glMap, layerOrId: string | TypedStyleLayer, opacity: number, cb?: () => void) { + let mllayer; + if (typeof layerOrId === 'string') { + mllayer = map.getLayer(layerOrId) as TypedStyleLayer | undefined; + } else { + mllayer = layerOrId; + } + if (mllayer) { + let type: any = mllayer.type; + if (mllayer.type === 'symbol') { + type = 'icon'; + } + + let opacityPaintProperty = `${type}-opacity`; + + if (mllayer.type === 'circle') { + opacityPaintProperty = 'circle-stroke-opacity'; + } + + // hillshade only has visibility + // https://github.com/maplibre/maplibre-gl-js/issues/1439 + if (mllayer.type === 'hillshade') { + opacityPaintProperty = 'hillshade-exaggeration'; + } + + if (mllayer.getPaintProperty(opacityPaintProperty) !== opacity) { + // TODO: custom layers -- On custom layers, only the group is set, not the layers, so they can be controlled by the user + // TODO: layerOrGroupSetOpacity(mllayer, layer.opacity, layer instanceof CustomLayer); + map.setPaintProperty(mllayer.id, opacityPaintProperty, opacity); + + // https://github.com/maplibre/maplibre-gl-js/issues/3001 + /* map.setLayoutProperty(mllayer.id, 'visibility', 'none'); + setTimeout(() => { + map.setLayoutProperty(mllayer.id, 'visibility', 'visible'); + }, 200); */ + //------------------------------------------------------- + } + } +} + +export function getOpacity(map: glMap, layerOrId: string | TypedStyleLayer) { + let mllayer; + if (typeof layerOrId === 'string') { + mllayer = map.getLayer(layerOrId) as TypedStyleLayer | undefined; + } else { + mllayer = layerOrId; + } + if (mllayer) { + return styleLayerGetOpacity(mllayer); + } +} + +// https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.StyleLayer/ +export function styleLayerGetOpacity(mllayer: TypedStyleLayer) { + let opacityPaintProperty = getOpacityPaintProperty(mllayer.type); + return mllayer.getPaintProperty(opacityPaintProperty); +} + +export function getOpacityPaintProperty(type: string) { + let _type = type; + if (type === 'symbol') { + _type = 'icon'; + } + + let opacityPaintProperty = `${_type}-opacity`; + + if (type === 'circle') { + opacityPaintProperty = 'circle-stroke-opacity'; + } + + // hillshade only has visibility + // https://github.com/maplibre/maplibre-gl-js/issues/1439 + if (type === 'hillshade') { + opacityPaintProperty = 'hillshade-exaggeration'; + } + + return opacityPaintProperty; +} + +export function getAllLayers(map: glMap, filtertype?: Tgroupfiltertype) { + const layers = map.getStyle().layers; + let filteredlayers = layers; + if (filtertype) { + const lowerType = filtertype.toLowerCase() as Tgroupfiltertype; + filteredlayers = layers.filter(l => (l.metadata as any)?.[UKIS_METADATA.filtertype]?.toLowerCase() === lowerType); + } + + return filteredlayers; +} + + +export function getUkisLayerIDs(map: glMap, filtertype?: Tgroupfiltertype) { + let filteredlayers = getAllLayers(map, filtertype) + const ids: string[] = filteredlayers.filter(l => (l.metadata as any)?.[UKIS_METADATA.layerID]).map(l => (l.metadata as any)?.[UKIS_METADATA.layerID]); + return [...new Set(ids)]; +} + +export function getLayersAndSources(map: glMap, ukisLayerID: string) { + const allLayers = getAllLayers(map); + const filtered = allLayers.filter(l => { + if ((l.metadata as any)?.[UKIS_METADATA.layerID] === ukisLayerID) { + return true; + } else { + return; + } + }); + const styleSources = map.getStyle().sources; + const filteredSources = filtered.reduce((results, l) => { + let sid: string; + if ('source' in l && typeof l['source'] === 'string') { + sid = l['source']; + } else { + sid = l.id; + } + + const s = styleSources[sid]; + if (s) { + results[sid] = s; + } + + return results + }, {} as SourceIdSpecification); + return { + layers: filtered, + sources: filteredSources + } +} + +export function removeLayerAndSource(map: glMap, ukisLayerID: string | string[]) { + const toRemove: { + layers: LayerSpecification[], + sources: SourceIdSpecification + } = { layers: [], sources: {} }; + + let groupIds = []; + const allLayers = getAllLayers(map); + if (Array.isArray(ukisLayerID)) { + groupIds = ukisLayerID; + } else { + groupIds.push(ukisLayerID); + } + + groupIds.forEach(item => { + const ls = getLayersAndSources(map, item); + toRemove.layers.push(...ls.layers); + toRemove.sources = ls.sources; + }); + + toRemove.layers.forEach(l => { + if (map.getLayer(l.id)) { + map.removeLayer(l.id); + } + }); + + const sourcesInOtherLayers = allLayers + .filter(l => !toRemove.layers.map(r => r.id).includes(l.id)) // Difference + .map(l => (l as any)?.source) // get sources + .filter((value, index, array) => array.indexOf(value) === index && value); // unique and not undefined + // only remove source if not used by other layer !!! + Object.keys(toRemove.sources).forEach(k => { + if (map.getSource(k) && !sourcesInOtherLayers.includes(k)) { + map.removeSource(k); + } + }); +} + + +/** + * Detect changes in layer order + */ +export function getLayerChangeOrder(layers: ukisLayer[], mapLayerIds: string[]) { + let orderChanges: { + layerId: string, + beforeId: string + }[] = []; + + let layersLength = mapLayerIds.length; + let index = layersLength; + while (index--) { + const mapLayer = mapLayerIds[index]; + const layer = layers[index]; + + if (mapLayer !== layer.id) { + const orderChange = { + layerId: layer.id, + beforeId: null as any + }; + + + /** + * https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.Map/#movelayer + * The ID of an existing layer to insert the new layer before. + * When viewing the map, layer.id will appear beneath the beforeId layer. + * If beforeId is omitted, the layer will be appended to the end of the layers array and appear above all other layers on the map. + */ + const beforeIndex = index + 1; + if (beforeIndex < layersLength) { + const beforeId = layers[beforeIndex].id; + orderChange.beforeId = beforeId; + } else if (beforeIndex === 0) { + const beforeId = layers[beforeIndex].id; + orderChange.beforeId = beforeId; + } + + orderChanges.push(orderChange); + } + + } + return orderChanges; +} + + +/** + * Change the order of map layers based on the new ukisLayers + */ +export function changeOrderOfLayers(map: glMap, layers: ukisLayer[], mapLayerIds: string[], filtertype: Tgroupfiltertype) { + const layerChange = getLayerChangeOrder(layers, mapLayerIds); + const length = layerChange.length; + if (length) { + for (let index = 0; index < length; index++) { + const lc = layerChange[index]; + if (index >= 1) { + const newMapLayerIds = getUkisLayerIDs(map, filtertype) + const newlayerChange = getLayerChangeOrder(layers, newMapLayerIds); + // Stop moving layers because the order is already the same as in the new layer array. + if (newlayerChange.length === 0) { + break; + } + } + changeOrderOfLayer(map, lc); + } + } +} + +export function changeOrderOfLayer(map: glMap, layerChange: { layerId: string, beforeId: string }) { + if (layerChange) { + const layerMapLayers = getLayersAndSources(map, layerChange.layerId).layers; + const beforeMapLayers = getLayersAndSources(map, layerChange.beforeId).layers; + /* const layerMapLayers = getFirstAndLastLayer(map, layerChange.layerId); + const beforeMapLayers = getFirstAndLastLayer(map, layerChange.beforeId); */ + /** + * if the layer before the one to be moved has several layers, move the layer on beforeMapLayers[0] + * If there is no layer before, move it to the top. + * + * https://maplibre.org/maplibre-gl-js/docs/API/classes/maplibregl.Map/#movelayer + * - If beforeId is omitted, the layer will be appended to the end of the layers array... - + */ + if (beforeMapLayers.length >= 1) { + layerChange.beforeId = beforeMapLayers[0].id; + } else { + layerChange.beforeId = null; + } + + + /** If the layer which should be moved has several layers, move all of them. */ + if (layerMapLayers.length > 1) { + // reverse to move + layerMapLayers.reverse(); + layerMapLayers.forEach((value: LayerSpecification, index: number) => { + // Move the first layer to the Before ID and then move all layer after the moved layer. + if (index === 0) { + if (layerChange.beforeId) { + map.moveLayer(value.id, layerChange.beforeId); + } else { + map.moveLayer(value.id); + } + } else { + const beforeLayer = layerMapLayers[index - 1]; + map.moveLayer(value.id, beforeLayer.id); + } + }); + } else if (layerMapLayers.length === 1) { + const layer = layerMapLayers[0]; + if (layerChange.beforeId) { + map.moveLayer(layer.id, layerChange.beforeId); + } else { + map.moveLayer(layer.id) + } + } else { + // layerMapLayers.length === 0 + // there is nothing to move + } + } +} \ No newline at end of file diff --git a/projects/map-maplibre/src/public-api.ts b/projects/map-maplibre/src/public-api.ts new file mode 100644 index 000000000..5cb21e467 --- /dev/null +++ b/projects/map-maplibre/src/public-api.ts @@ -0,0 +1,9 @@ +/* + * Public API Surface of map-maplibre + */ + +export * from './lib/maplibre.helpers'; +export * from './lib/maplibre-layers.helpers'; +export * from './lib/map-maplibre.service'; +export * from './lib/map-maplibre.component'; +export * from './lib/map-maplibre.module'; diff --git a/projects/map-maplibre/src/test.ts b/projects/map-maplibre/src/test.ts new file mode 100644 index 000000000..5775317ab --- /dev/null +++ b/projects/map-maplibre/src/test.ts @@ -0,0 +1,27 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + (id: string): T; + keys(): string[]; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().forEach(context); diff --git a/projects/map-maplibre/tsconfig.lib.json b/projects/map-maplibre/tsconfig.lib.json new file mode 100644 index 000000000..b77b13c01 --- /dev/null +++ b/projects/map-maplibre/tsconfig.lib.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/projects/map-maplibre/tsconfig.lib.prod.json b/projects/map-maplibre/tsconfig.lib.prod.json new file mode 100644 index 000000000..06de549e1 --- /dev/null +++ b/projects/map-maplibre/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/projects/map-maplibre/tsconfig.spec.json b/projects/map-maplibre/tsconfig.spec.json new file mode 100644 index 000000000..715dd0a5d --- /dev/null +++ b/projects/map-maplibre/tsconfig.spec.json @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/projects/map-ol/src/lib/map-ol.component.ts b/projects/map-ol/src/lib/map-ol.component.ts index 0019a7f0a..08de44f63 100644 --- a/projects/map-ol/src/lib/map-ol.component.ts +++ b/projects/map-ol/src/lib/map-ol.component.ts @@ -382,6 +382,7 @@ export class MapOlComponent implements OnInit, AfterViewInit, AfterViewChecked, } } + // TODO: replace with @dlr-eoc/utilities propsEqual() private shallowEqual(a: object, b: object): boolean { // Create arrays of property names const aProps = Object.getOwnPropertyNames(a);