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 000000000..1aabff6c4
Binary files /dev/null and b/projects/demo-maps/src/assets/route-maplibre.jpg differ
diff --git a/projects/map-maplibre/README.md b/projects/map-maplibre/README.md
new file mode 100644
index 000000000..fb85c262a
--- /dev/null
+++ b/projects/map-maplibre/README.md
@@ -0,0 +1,138 @@
+# @dlr-eoc/map-maplibre
+
+### how to use this in a ukis-angular (@dlr-eoc/core-ui) project
+
+For examples [see demo maps](../demo-maps/README.md)
+
+#### add the following dependencies to the package.json
+- "@dlr-eoc/map-maplibre"
+- "@dlr-eoc/layer-control" (optional)
+- "@dlr-eoc/base-layers-raster" (optional)
+
+
+### add styles from maplibre to your application
+
+e.g. in your apps style file
+```
+// styles.scss/styles.css
+@import 'maplibre-gl/dist/maplibre-gl.css';
+...
+
+```
+
+or in the angular config file
+```
+// angular.json
+...
+ "styles": [
+ ...
+ "node_modules/maplibre-gl/dist/maplibre-gl.css",
+ "src/styles.scss"
+ ],
+...
+
+```
+
+
+#### add the following to the app.module.ts
+```
+import { MapMaplibreModule } from '@dlr-eoc/map-maplibre';
+import { LayerControlModule } from '@dlr-eoc/layer-control';
+
+...
+
+ imports: [
+ ...
+ MapMaplibreModule,
+ LayerControlModule
+ ]
+```
+
+
+#### add the following to a route-view.component.html
+```
+
+```
+
+#### 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);