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);