diff --git a/package-lock.json b/package-lock.json index 6f42bd0976df8e01c2764dd4c37253b89ed17558..b626982eea0e16bfe5bf9f8eef8a95f8a3b1200a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "molart": "1.15.0", "next": "13.4.19", "ol": "10.2.0", + "ol-ext": "4.0.24", "polished": "4.3.1", "postcss": "8.4.29", "query-string": "7.1.3", @@ -56,6 +57,7 @@ "@types/crypto-js": "4.2.2", "@types/is-uuid": "1.0.2", "@types/jest": "29.5.11", + "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.6.1", "@types/react-autosuggest": "^10.1.11", "@types/react-redux": "7.1.33", "@types/redux-mock-store": "1.0.6", @@ -2562,6 +2564,16 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "node_modules/@types/ol-ext": { + "name": "@siedlerchr/types-ol-ext", + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@siedlerchr/types-ol-ext/-/types-ol-ext-3.6.1.tgz", + "integrity": "sha512-CSqgXdS1d028TvnVXf2/dgnU66lRGHM9+R5E8lL2ilmm6ktHVZtsIhRR8rWSXu4Ybt1rGdMbwbDxofIbilWIyQ==", + "dev": true, + "peerDependencies": { + "jspdf": "^2.5.2" + } + }, "node_modules/@types/openlayers": { "version": "4.6.23", "resolved": "https://registry.npmjs.org/@types/openlayers/-/openlayers-4.6.23.tgz", @@ -2577,6 +2589,14 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/@types/rbush": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.3.tgz", @@ -3317,6 +3337,19 @@ "node": ">= 4.0.0" } }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "peer": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, "node_modules/attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -3609,6 +3642,17 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -3729,6 +3773,19 @@ "node-int64": "^0.4.0" } }, + "node_modules/btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "dev": true, + "peer": true, + "bin": { + "btoa": "bin/btoa.js" + }, + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -3892,6 +3949,35 @@ } ] }, + "node_modules/canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/canvg/node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -4519,6 +4605,19 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "node_modules/core-js": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -4610,6 +4709,17 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -5248,6 +5358,14 @@ "node": ">=12" } }, + "node_modules/dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -6684,6 +6802,13 @@ "pend": "~1.2.0" } }, + "node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "peer": true + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -7399,6 +7524,21 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -9552,6 +9692,25 @@ "node": "*" } }, + "node_modules/jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "html2canvas": "^1.0.0-rc.5" + } + }, "node_modules/jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -11001,6 +11160,14 @@ "url": "https://opencollective.com/openlayers" } }, + "node_modules/ol-ext": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/ol-ext/-/ol-ext-4.0.24.tgz", + "integrity": "sha512-VEf1+mjvbe35mMsszVsugqcvWfeGcU8TwS+GgXm3nGYqiHR7CckX2DWmM9B94QCDnrJWKKXBicfInbkoe2xT7w==", + "peerDependencies": { + "ol": ">= 5.3.0" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -11837,6 +12004,17 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/randexp": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", @@ -12408,6 +12586,17 @@ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", "dev": true }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -12848,6 +13037,17 @@ "node": ">=8" } }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -13224,6 +13424,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -13333,6 +13544,17 @@ "node": ">=0.10" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -13823,6 +14045,17 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", @@ -16276,6 +16509,13 @@ "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "dev": true }, + "@types/ol-ext": { + "version": "npm:@siedlerchr/types-ol-ext@3.6.1", + "resolved": "https://registry.npmjs.org/@siedlerchr/types-ol-ext/-/types-ol-ext-3.6.1.tgz", + "integrity": "sha512-CSqgXdS1d028TvnVXf2/dgnU66lRGHM9+R5E8lL2ilmm6ktHVZtsIhRR8rWSXu4Ybt1rGdMbwbDxofIbilWIyQ==", + "dev": true, + "requires": {} + }, "@types/openlayers": { "version": "4.6.23", "resolved": "https://registry.npmjs.org/@types/openlayers/-/openlayers-4.6.23.tgz", @@ -16291,6 +16531,14 @@ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==" }, + "@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "dev": true, + "optional": true, + "peer": true + }, "@types/rbush": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/rbush/-/rbush-3.0.3.tgz", @@ -16824,6 +17072,13 @@ "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", "dev": true }, + "atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "peer": true + }, "attr-accept": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", @@ -17037,6 +17292,14 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "dev": true, + "optional": true, + "peer": true + }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -17117,6 +17380,13 @@ "node-int64": "^0.4.0" } }, + "btoa": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", + "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==", + "dev": true, + "peer": true + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -17218,6 +17488,34 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001572.tgz", "integrity": "sha512-1Pbh5FLmn5y4+QhNyJE9j3/7dK44dGB83/ZMjv/qJk86TvDbjk0LosiZo0i0WB0Vx607qMX9jYrn1VLHCkN4rw==" }, + "canvg": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.10.tgz", + "integrity": "sha512-qwR2FRNO9NlzTeKIPIKpnTY6fqwuYSequ8Ru8c0YkYU7U0oW+hLUvWadLvAu1Rl72OMNiFhoLu4f8eUjQ7l/+Q==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true, + "optional": true, + "peer": true + } + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -17692,6 +17990,14 @@ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, + "core-js": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.40.0.tgz", + "integrity": "sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==", + "dev": true, + "optional": true, + "peer": true + }, "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -17752,6 +18058,17 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, + "css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "utrie": "^1.0.2" + } + }, "css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -18254,6 +18571,14 @@ "webidl-conversions": "^7.0.0" } }, + "dompurify": { + "version": "2.5.8", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-2.5.8.tgz", + "integrity": "sha512-o1vSNgrmYMQObbSSvF/1brBYEQPHhV1+gsmrusO7/GXtp1T9rCS8cXFqVxK/9crT1jA6Ccv+5MTSjBNqr7Sovw==", + "dev": true, + "optional": true, + "peer": true + }, "dot-prop": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", @@ -19280,6 +19605,13 @@ "pend": "~1.2.0" } }, + "fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "peer": true + }, "figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -19792,6 +20124,18 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + } + }, "http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -21332,6 +21676,23 @@ "through": ">=2.2.7 <3" } }, + "jspdf": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.2.tgz", + "integrity": "sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/runtime": "^7.23.2", + "atob": "^2.1.2", + "btoa": "^1.2.1", + "canvg": "^3.0.6", + "core-js": "^3.6.0", + "dompurify": "^2.5.4", + "fflate": "^0.8.1", + "html2canvas": "^1.0.0-rc.5" + } + }, "jsprim": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-2.0.2.tgz", @@ -22385,6 +22746,12 @@ "rbush": "^4.0.0" } }, + "ol-ext": { + "version": "4.0.24", + "resolved": "https://registry.npmjs.org/ol-ext/-/ol-ext-4.0.24.tgz", + "integrity": "sha512-VEf1+mjvbe35mMsszVsugqcvWfeGcU8TwS+GgXm3nGYqiHR7CckX2DWmM9B94QCDnrJWKKXBicfInbkoe2xT7w==", + "requires": {} + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -22905,6 +23272,17 @@ "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==" }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "performance-now": "^2.1.0" + } + }, "randexp": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.5.3.tgz", @@ -23329,6 +23707,14 @@ "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", "dev": true }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "dev": true, + "optional": true, + "peer": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -23658,6 +24044,14 @@ } } }, + "stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "dev": true, + "optional": true, + "peer": true + }, "stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -23923,6 +24317,14 @@ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==" }, + "svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "dev": true, + "optional": true, + "peer": true + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -24005,6 +24407,17 @@ "integrity": "sha512-wiBrwC1EhBelW12Zy26JeOUkQ5mRu+5o8rpsJk5+2t+Y5vE7e842qtZDQ2g1NpX/29HdyFeJ4nSIhI47ENSxlQ==", "dev": true }, + "text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "utrie": "^1.0.2" + } + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -24355,6 +24768,17 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "base64-arraybuffer": "^1.0.2" + } + }, "uuid": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", diff --git a/package.json b/package.json index 52f25ca960a70a3e6c7eb204f2519242adfd512c..1d5e4494e78b9aec83be16ff9ceb0dcdbc4c17ae 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "molart": "1.15.0", "next": "13.4.19", "ol": "10.2.0", + "ol-ext": "4.0.24", "polished": "4.3.1", "postcss": "8.4.29", "query-string": "7.1.3", @@ -62,6 +63,7 @@ "zod-to-json-schema": "3.22.4" }, "devDependencies": { + "@types/ol-ext": "npm:@siedlerchr/types-ol-ext@3.6.1", "@commitlint/cli": "17.8.1", "@commitlint/config-conventional": "17.8.1", "@testing-library/jest-dom": "6.1.6", diff --git a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx index 50deda14baae2d6318e793162819e7c106472dc6..e7a0139374792c66da09cc71af07b038a0752be9 100644 --- a/src/components/Map/MapDrawActions/MapDrawActions.component.tsx +++ b/src/components/Map/MapDrawActions/MapDrawActions.component.tsx @@ -37,6 +37,12 @@ export const MapDrawActions = (): React.JSX.Element | null => { icon="image" title="Draw image" /> + <MapDrawActionsButton + isActive={activeAction === MAP_EDIT_ACTIONS.TRANSFORM_IMAGE} + toggleMapEditAction={() => toggleMapEditAction(MAP_EDIT_ACTIONS.TRANSFORM_IMAGE)} + icon="resize-image" + title="Transform image" + /> </div> ); }; diff --git a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts index 63fddd60951fe9f46d1f628fbf562d7438516440..709336b14c2a6815994e21ebd1e24e138b5384ce 100644 --- a/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts +++ b/src/components/Map/MapViewer/MapViewerVector/MapViewerVector.types.ts @@ -15,3 +15,10 @@ export type ScaleFunction = (resolution: number) => number; export type OverlayBioEntityGroupedElementsType = { [id: string]: Array<OverlayBioEntityRender & { amount: number }>; }; + +export type BoundingBox = { + x: number; + y: number; + width: number; + height: number; +}; diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts index fb720a2799702c4a049aa1ca3ed4ba0c6db8d41b..11211b8bb13f926f939c3d76935376153149b2b7 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.test.ts @@ -7,10 +7,11 @@ import { handleFeaturesClick } from '@/components/Map/MapViewer/utils/listeners/ import Map from 'ol/Map'; import { onMapLeftClick } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick'; import { Comment } from '@/types/models'; -import { Layer } from 'ol/layer'; import SimpleGeometry from 'ol/geom/SimpleGeometry'; import { Feature } from 'ol'; import { FEATURE_TYPE } from '@/constants/features'; +import VectorLayer from 'ol/layer/Vector'; +import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import * as leftClickHandleAlias from './leftClickHandleAlias'; import * as clickHandleReaction from '../clickHandleReaction'; @@ -33,6 +34,8 @@ describe('onMapLeftClick', () => { const isResultDrawerOpen = true; const comments: Array<Comment> = []; let mapInstance: Map; + const vectorLayer = new VectorLayer({}); + vectorLayer.set('type', VECTOR_MAP_LAYER_TYPE); const event = { coordinate: [100, 50], pixel: [200, 100] }; const mapSize = { width: 90, @@ -51,11 +54,7 @@ describe('onMapLeftClick', () => { it('dispatches updateLastClick and resets data if no feature at pixel', async () => { const dispatch = jest.fn(); jest.spyOn(mapInstance, 'forEachFeatureAtPixel').mockImplementation((_, callback) => { - callback( - new Feature({ zIndex: 1 }), - null as unknown as Layer, - null as unknown as SimpleGeometry, - ); + callback(new Feature({ zIndex: 1 }), vectorLayer, null as unknown as SimpleGeometry); }); await onMapLeftClick( mapSize, @@ -80,7 +79,7 @@ describe('onMapLeftClick', () => { })); const feature = new Feature({ id: 1, type: FEATURE_TYPE.ALIAS, zIndex: 1 }); jest.spyOn(mapInstance, 'forEachFeatureAtPixel').mockImplementation((_, callback) => { - callback(feature, null as unknown as Layer, null as unknown as SimpleGeometry); + callback(feature, vectorLayer, null as unknown as SimpleGeometry); }); (handleFeaturesClick as jest.Mock).mockReturnValue({ shouldBlockCoordSearch: false }); @@ -104,7 +103,7 @@ describe('onMapLeftClick', () => { })); const feature = new Feature({ id: 1, type: FEATURE_TYPE.REACTION, zIndex: 1 }); jest.spyOn(mapInstance, 'forEachFeatureAtPixel').mockImplementation((_, callback) => { - callback(feature, null as unknown as Layer, null as unknown as SimpleGeometry); + callback(feature, vectorLayer, null as unknown as SimpleGeometry); }); (handleFeaturesClick as jest.Mock).mockReturnValue({ shouldBlockCoordSearch: false }); diff --git a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts index 3a33837b1a82b2f8999bc503374096f15babaf38..d9a8031ca12b0d5a5abda289469363ff766be43b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts +++ b/src/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/mouseLeftClick/onMapLeftClick.ts @@ -15,6 +15,7 @@ import { resetReactionsData } from '@/redux/reactions/reactions.slice'; import { handleDataReset } from '@/components/Map/MapViewer/utils/listeners/mapSingleClick/handleDataReset'; import { FEATURE_TYPE } from '@/constants/features'; import { clickHandleReaction } from '@/components/Map/MapViewer/MapViewerVector/listeners/mouseClick/clickHandleReaction'; +import { VECTOR_MAP_LAYER_TYPE } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; function isFeatureFilledCompartment(feature: FeatureLike): boolean { return feature.get('type') === FEATURE_TYPE.COMPARTMENT && feature.get('filled'); @@ -50,9 +51,10 @@ export const onMapLeftClick = let featureAtPixel: FeatureLike | undefined; mapInstance.forEachFeatureAtPixel( pixel, - feature => { + (feature, layer) => { const featureZIndex = feature.get('zIndex'); if ( + layer && layer.get('type') === VECTOR_MAP_LAYER_TYPE && (isFeatureFilledCompartment(feature) || isFeatureNotCompartment(feature)) && (featureZIndex === undefined || featureZIndex >= 0) ) { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts index 6f60435b25a935e5b0e3a5adf4968a5b01a1a2fe..ada5b18ea5048e2a90b37f74aae6c2cdcf19375b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/additionalLayers/useOlMapAdditionalLayers.ts @@ -1,5 +1,5 @@ /* eslint-disable no-magic-numbers */ -import { Feature } from 'ol'; +import { Collection, Feature } from 'ol'; import VectorLayer from 'ol/layer/Vector'; import VectorSource from 'ol/source/Vector'; import { useEffect, useMemo, useState } from 'react'; @@ -15,7 +15,7 @@ import { } from '@/redux/layers/layers.selectors'; import { usePointToProjection } from '@/utils/map/usePointToProjection'; import { MapInstance } from '@/types/map'; -import { LineString, MultiPolygon, Point } from 'ol/geom'; +import { Geometry, LineString, MultiPolygon, Point } from 'ol/geom'; import Polygon from 'ol/geom/Polygon'; import Layer from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer'; import { arrowTypesSelector, lineTypesSelector } from '@/redux/shapes/shapes.selectors'; @@ -26,6 +26,7 @@ import { LayerState } from '@/redux/layers/layers.types'; import { mapEditToolsActiveActionSelector } from '@/redux/mapEditTools/mapEditTools.selectors'; import { MAP_EDIT_ACTIONS } from '@/redux/mapEditTools/mapEditTools.constants'; import { Extent } from 'ol/extent'; +import getTransformImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction'; export const useOlMapAdditionalLayers = ( mapInstance: MapInstance, @@ -92,11 +93,12 @@ export const useOlMapAdditionalLayers = ( lineTypes, arrowTypes, mapInstance, + mapSize, pointToProjection, }); return additionalLayer.vectorLayer; }); - }, [layersState, lineTypes, arrowTypes, mapInstance, pointToProjection]); + }, [layersState, lineTypes, arrowTypes, mapInstance, mapSize, pointToProjection]); useEffect(() => { if (layersLoading === 'pending') { @@ -107,6 +109,25 @@ export const useOlMapAdditionalLayers = ( } }, [layersForCurrentModel, layersLoading, layersLoadingState]); + const transformInteraction = useMemo(() => { + if (!dispatch || !currentModelId || !activeLayer) { + return null; + } + let imagesFeatures: Collection<Feature<Geometry>> = new Collection(); + const vectorLayer = vectorLayers.find(layer => layer.get('id') === activeLayer); + if (vectorLayer) { + imagesFeatures = new Collection(vectorLayer.get('imagesFeatures')); + } + return getTransformImageInteraction( + dispatch, + mapSize, + currentModelId, + activeLayer, + imagesFeatures, + restrictionExtent, + ); + }, [dispatch, mapSize, currentModelId, activeLayer, vectorLayers, restrictionExtent]); + useEffect(() => { vectorLayers.forEach(layer => { const layerId = layer.get('id'); @@ -116,6 +137,19 @@ export const useOlMapAdditionalLayers = ( }); }, [layersVisibilityForCurrentModel, vectorLayers]); + useEffect(() => { + if (!transformInteraction) { + return () => {}; + } + if (!activeLayer || !vectorRendering || activeAction !== MAP_EDIT_ACTIONS.TRANSFORM_IMAGE) { + return () => {}; + } + mapInstance?.addInteraction(transformInteraction); + return () => { + mapInstance?.removeInteraction(transformInteraction); + }; + }, [activeAction, activeLayer, mapInstance, transformInteraction, vectorRendering]); + useEffect(() => { if (!drawImageInteraction) { return; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts index 2f5b97ba2fd9449bff6952895cc0ec603b81bf01..60746292152eec5fc819559449a89c7c19d1a342 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/config/reactionsLayer/processModelElements.ts @@ -46,6 +46,7 @@ export default function processModelElements( zIndex: element.z, pointToProjection, mapInstance, + mapSize, }); validElements.push(glyph); return; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..532c485dee1ef05dc9e188e3f600cc2c995d7fae --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.test.ts @@ -0,0 +1,26 @@ +/* eslint-disable no-magic-numbers */ +import { Extent } from 'ol/extent'; +import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent'; + +describe('getBoundingBoxFromExtent', () => { + it('should return a bounding box for extent', () => { + const extent: Extent = [0, 195700, 195700, 0]; + const mapSize = { + width: 512, + height: 512, + tileSize: 256, + minZoom: 2, + maxZoom: 4, + }; + + const result = getBoundingBoxFromExtent(extent, mapSize); + + expect(result).toHaveProperty('x', 1024); + expect(result).toHaveProperty('y', 1024); + expect(result).toHaveProperty('width'); + expect(result).toHaveProperty('height'); + + expect(result.width).toBeCloseTo(10); + expect(result.height).toBeCloseTo(10); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts new file mode 100644 index 0000000000000000000000000000000000000000..efab1e2d7b3cd6d02e193a5a151981854b90cb15 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent.ts @@ -0,0 +1,23 @@ +/* eslint-disable no-magic-numbers */ +import { MapSize } from '@/redux/map/map.types'; +import { toLonLat } from 'ol/proj'; +import { latLngToPoint } from '@/utils/map/latLngToPoint'; +import { Extent } from 'ol/extent'; +import { BoundingBox } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.types'; + +export default function getBoundingBoxFromExtent(extent: Extent, mapSize: MapSize): BoundingBox { + const [startLng, startLat] = toLonLat([extent[0], extent[3]]); + const startPoint = latLngToPoint([startLat, startLng], mapSize); + const [endLng, endLat] = toLonLat([extent[2], extent[1]]); + const endPoint = latLngToPoint([endLat, endLng], mapSize); + + const width = Math.abs(endPoint.x - startPoint.x); + const height = Math.abs(endPoint.y - startPoint.y); + + return { + width, + height, + x: startPoint.x, + y: startPoint.y, + }; +} diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts index 6ac8c5d29e0e6aa78f4b7edb05e1a2f61a626b25..bef1e03750a22e80784dc3272d1d9b4d55310096 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.test.ts @@ -13,6 +13,13 @@ describe('Glyph', () => { let glyph: Glyph; let mapInstance: MapInstance; let pointToProjectionMock: jest.MockedFunction<UsePointToProjectionResult>; + const mapSize = { + width: 90, + height: 90, + tileSize: 256, + minZoom: 2, + maxZoom: 9, + }; beforeEach(() => { const dummyElement = document.createElement('div'); @@ -37,6 +44,7 @@ describe('Glyph', () => { zIndex: 1, pointToProjection: pointToProjectionMock, mapInstance, + mapSize, }; glyph = new Glyph(props); }); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts index c7844ae36e3acae71caa5accfab855a3e1d06b40..53f71dc9c250d3ff04e593ece5a65b1b9440f115 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph.ts @@ -15,6 +15,9 @@ import getStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/st import { WHITE_COLOR } from '@/components/Map/MapViewer/MapViewerVector/MapViewerVector.constants'; import getFill from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getFill'; import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle'; +import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent'; +import { MapSize } from '@/redux/map/map.types'; +import { LayerImage } from '@/types/models'; export type GlyphProps = { elementId: number; @@ -26,6 +29,7 @@ export type GlyphProps = { zIndex: number; pointToProjection: UsePointToProjectionResult; mapInstance: MapInstance; + mapSize: MapSize; }; export default class Glyph { @@ -39,6 +43,10 @@ export default class Glyph { polygonStyle: Style; + polygon: Polygon = new Polygon([]); + + elementId: number; + width: number; height: number; @@ -47,6 +55,10 @@ export default class Glyph { y: number; + zIndex: number; + + glyphId: number | null; + widthOnMap: number; heightOnMap: number; @@ -55,8 +67,20 @@ export default class Glyph { minResolution: number; + imageWidth: number = 1; + + imageHeight: number = 1; + + imageWidthOnMap: number = 1; + + imageHeightOnMap: number = 1; + + mapInstance: MapInstance; + pointToProjection: UsePointToProjectionResult; + mapSize: MapSize; + constructor({ elementId, glyphId, @@ -67,11 +91,17 @@ export default class Glyph { zIndex, pointToProjection, mapInstance, + mapSize, }: GlyphProps) { + this.elementId = elementId; this.width = width; this.height = height; + this.mapSize = mapSize; + this.glyphId = glyphId; this.x = x; this.y = y; + this.zIndex = zIndex; + this.mapInstance = mapInstance; this.pointToProjection = pointToProjection; const point1 = this.pointToProjection({ x: 0, y: 0 }); const point2 = this.pointToProjection({ x: this.width, y: this.height }); @@ -81,26 +111,19 @@ export default class Glyph { const maxZoom = mapInstance?.getView().get('originalMaxZoom'); this.minResolution = mapInstance?.getView().getResolutionForZoom(maxZoom) || 1; this.pixelRatio = this.widthOnMap / this.minResolution / this.width; - const polygon = new Polygon([ - [ - pointToProjection({ x, y }), - pointToProjection({ x: x + width, y }), - pointToProjection({ x: x + width, y: y + height }), - pointToProjection({ x, y: y + height }), - pointToProjection({ x, y }), - ], - ]); + + this.drawPolygon(); this.polygonStyle = getStyle({ - geometry: polygon, - zIndex, + geometry: this.polygon, + zIndex: this.zIndex, borderColor: { ...WHITE_COLOR, alpha: 0 }, fillColor: { ...WHITE_COLOR, alpha: 0 }, }); this.noGlyphStyle = getStyle({ - geometry: polygon, - zIndex, + geometry: this.polygon, + zIndex: this.zIndex, fillColor: '#E7E7E7', }); this.noGlyphStyle.setText( @@ -113,61 +136,127 @@ export default class Glyph { ); this.feature = new Feature({ - geometry: polygon, - id: elementId, + geometry: this.polygon, + id: this.elementId, type: FEATURE_TYPE.GLYPH, - zIndex, - getAnchorAndCoords: (): { anchor: Array<number>; coords: Coordinate } => { - const center = mapInstance?.getView().getCenter(); + zIndex: this.zIndex, + getAnchorAndCoords: (coords: Coordinate): { anchor: Array<number>; coords: Coordinate } => { + const center = this.mapInstance?.getView().getCenter(); let anchorX = 0; let anchorY = 0; if (center) { - anchorX = - (center[0] - this.pointToProjection({ x: this.x, y: this.y })[0]) / this.widthOnMap; - anchorY = - -(center[1] - this.pointToProjection({ x: this.x, y: this.y })[1]) / this.heightOnMap; + anchorX = (center[0] - coords[0]) / this.widthOnMap; + anchorY = -(center[1] - coords[1]) / this.heightOnMap; } return { anchor: [anchorX, anchorY], coords: center || [0, 0] }; }, }); + this.feature.set('setCoordinates', this.setCoordinates.bind(this)); + this.feature.set('getGlyphData', this.getGlyphData.bind(this)); + this.feature.set('reset', this.reset.bind(this)); this.feature.setStyle(this.getStyle.bind(this)); - if (!glyphId) { + + if (!this.glyphId) { return; } const img = new Image(); img.onload = (): void => { - const imageWidth = img.naturalWidth; - const imageHeight = img.naturalHeight; - const heightScale = height / imageHeight; - const widthScale = width / imageWidth; - if (heightScale < widthScale) { - this.imageScale = heightScale; - this.widthOnMap = (this.heightOnMap * imageWidth) / imageHeight; - } else { - this.imageScale = widthScale; - this.heightOnMap = (this.widthOnMap * imageHeight) / imageWidth; - } + this.imageWidth = img.naturalWidth; + this.imageHeight = img.naturalHeight; + const imagePoint1 = this.pointToProjection({ x: 0, y: 0 }); + const imagePoint2 = this.pointToProjection({ x: this.imageWidth, y: this.imageHeight }); + this.imageWidthOnMap = Math.abs(imagePoint2[0] - imagePoint1[0]); + this.imageHeightOnMap = Math.abs(imagePoint2[1] - imagePoint1[1]); + this.setImageScaleAndDimensions(this.heightOnMap, this.widthOnMap); this.style = new Style({ image: new Icon({ anchor: [0, 0], img, - size: [imageWidth, imageHeight], + size: [this.imageWidth, this.imageHeight], }), - zIndex, + zIndex: this.zIndex, }); }; - img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(glyphId)}`; + img.src = `${BASE_NEW_API_URL}${apiPath.getGlyphImage(this.glyphId)}`; + } + + private drawPolygon(): void { + this.polygon = new Polygon([ + [ + this.pointToProjection({ x: this.x, y: this.y }), + this.pointToProjection({ x: this.x + this.width, y: this.y }), + this.pointToProjection({ x: this.x + this.width, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y + this.height }), + this.pointToProjection({ x: this.x, y: this.y }), + ], + ]); + } + + private reset(): void { + this.drawPolygon(); + this.polygonStyle.setGeometry(this.polygon); + this.feature.setGeometry(this.polygon); + } + + protected setImageScaleAndDimensions(height: number, width: number): void { + this.widthOnMap = width; + this.heightOnMap = height; + const heightScale = height / this.imageHeightOnMap; + const widthScale = width / this.imageWidthOnMap; + if (heightScale < widthScale) { + this.imageScale = heightScale; + this.widthOnMap = (this.heightOnMap * this.imageWidth) / this.imageHeight; + } else { + this.imageScale = widthScale; + this.heightOnMap = (this.widthOnMap * this.imageHeight) / this.imageWidth; + } + } + + private setCoordinates(coords: Coordinate[][]): void { + const geometry = this.polygonStyle.getGeometry(); + if (geometry && geometry instanceof Polygon) { + geometry.setCoordinates(coords); + const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), this.mapSize); + this.x = boundingBox.x; + this.y = boundingBox.y; + this.width = boundingBox.width; + this.height = boundingBox.height; + } + } + + private getGlyphData(): LayerImage { + return { + id: this.elementId, + x: this.x, + y: this.y, + width: this.width, + height: this.height, + glyph: this.glyphId, + z: this.zIndex, + }; } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { const scale = this.minResolution / resolution; const getAnchorAndCoords = feature.get('getAnchorAndCoords'); let anchor = [0, 0]; - let coords = this.pointToProjection({ x: this.x, y: this.y }); + let coords = [this.x, this.y]; + const geometry = feature.getGeometry(); + if (geometry && geometry instanceof Polygon) { + const polygonExtent = geometry.getExtent(); + if (polygonExtent) { + coords = [polygonExtent[0], polygonExtent[3]]; + const width = Math.abs(polygonExtent[0] - polygonExtent[2]); + const height = Math.abs(polygonExtent[1] - polygonExtent[3]); + this.setImageScaleAndDimensions(height, width); + } + } else { + return []; + } if (getAnchorAndCoords instanceof Function) { - const anchorAndCoords = getAnchorAndCoords(); + const anchorAndCoords = getAnchorAndCoords(coords); anchor = anchorAndCoords.anchor; coords = anchorAndCoords.coords; } diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts index 1cdd1aef0edf1bfae3d0b785eb3080420c2e3fe9..9da93414263578fa50ea5684ac1918f78dd1e5b5 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.test.ts @@ -23,6 +23,13 @@ jest.mock('../style/rgbToHex'); describe('Layer', () => { let props: LayerProps; + const mapSize = { + width: 90, + height: 90, + tileSize: 256, + minZoom: 2, + maxZoom: 9, + }; beforeEach(() => { const dummyElement = document.createElement('div'); @@ -128,6 +135,7 @@ describe('Layer', () => { layerId: 23, pointToProjection: jest.fn(point => [point.x, point.y]), mapInstance, + mapSize, lineTypes: {}, arrowTypes: {}, }; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts index e48faf93c38db11b282b23a3a87e253979eee661..65c6f6132f43884169e4963ee1d3eb7d00cc873b 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/Layer.ts @@ -27,6 +27,7 @@ import { import getScaledElementStyle from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/style/getScaledElementStyle'; import { Stroke } from 'ol/style'; import Glyph from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/elements/Glyph'; +import { MapSize } from '@/redux/map/map.types'; export interface LayerProps { texts: Array<LayerText>; @@ -39,10 +40,13 @@ export interface LayerProps { lineTypes: LineTypeDict; arrowTypes: ArrowTypeDict; mapInstance: MapInstance; + mapSize: MapSize; pointToProjection: UsePointToProjectionResult; } export default class Layer { + layerId: number; + texts: Array<LayerText>; rects: Array<LayerRect>; @@ -61,6 +65,8 @@ export default class Layer { mapInstance: MapInstance; + mapSize: MapSize; + vectorSource: VectorSource< Feature<Point> | Feature<Polygon> | Feature<LineString> | Feature<MultiPolygon> >; @@ -80,6 +86,7 @@ export default class Layer { lineTypes, arrowTypes, mapInstance, + mapSize, pointToProjection, }: LayerProps) { this.vectorSource = new VectorSource({}); @@ -93,11 +100,14 @@ export default class Layer { this.arrowTypes = arrowTypes; this.pointToProjection = pointToProjection; this.mapInstance = mapInstance; + this.mapSize = mapSize; + this.layerId = layerId; this.vectorSource.addFeatures(this.getTextsFeatures()); this.vectorSource.addFeatures(this.getRectsFeatures()); this.vectorSource.addFeatures(this.getOvalsFeatures()); - this.drawImages(); + const imagesFeatures = this.getImagesFeatures(); + this.vectorSource.addFeatures(imagesFeatures); const { linesFeatures, arrowsFeatures } = this.getLinesFeatures(); this.vectorSource.addFeatures(linesFeatures); @@ -111,6 +121,7 @@ export default class Layer { }); this.vectorLayer.set('id', layerId); + this.vectorLayer.set('imagesFeatures', imagesFeatures); this.vectorLayer.set('drawImage', this.drawImage.bind(this)); } @@ -290,13 +301,22 @@ export default class Layer { return { linesFeatures, arrowsFeatures }; }; - private drawImages(): void { - Object.values(this.images).forEach(image => { - this.drawImage(image); + private getImagesFeatures(): Feature<Polygon>[] { + return Object.values(this.images).map(image => { + return this.getGlyphFeature(image); }); } private drawImage(image: LayerImage): void { + const glyphFeature = this.getGlyphFeature(image); + const imagesFeatures = this.vectorLayer.get('imagesFeatures'); + if (imagesFeatures && Array.isArray(imagesFeatures)) { + imagesFeatures.push(glyphFeature); + } + this.vectorSource.addFeature(glyphFeature); + } + + private getGlyphFeature(image: LayerImage): Feature<Polygon> { const glyph = new Glyph({ elementId: image.id, glyphId: image.glyph, @@ -307,8 +327,9 @@ export default class Layer { zIndex: image.z, pointToProjection: this.pointToProjection, mapInstance: this.mapInstance, + mapSize: this.mapSize, }); - this.vectorSource.addFeature(glyph.feature); + return glyph.feature; } protected getStyle(feature: FeatureLike, resolution: number): Style | Array<Style> | void { diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts index 55a459bcc74378a7a0e2cd289c27050308a7e4ea..35bd90a5543d3a96015ac202732164dd55e806fe 100644 --- a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getDrawImageInteraction.ts @@ -2,13 +2,12 @@ import Draw from 'ol/interaction/Draw'; import SimpleGeometry from 'ol/geom/SimpleGeometry'; import Polygon from 'ol/geom/Polygon'; -import { toLonLat } from 'ol/proj'; -import { latLngToPoint } from '@/utils/map/latLngToPoint'; import { MapSize } from '@/redux/map/map.types'; import { AppDispatch } from '@/redux/store'; import { Coordinate } from 'ol/coordinate'; import { openLayerImageObjectFactoryModal } from '@/redux/modal/modal.slice'; import { Extent } from 'ol/extent'; +import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent'; export default function getDrawImageInteraction( mapSize: MapSize, @@ -70,25 +69,13 @@ export default function getDrawImageInteraction( const geometry = event.feature.getGeometry() as Polygon; const extent = geometry.getExtent(); - const [startLng, startLat] = toLonLat([extent[0], extent[3]]); - const startPoint = latLngToPoint([startLat, startLng], mapSize); - const [endLng, endLat] = toLonLat([extent[2], extent[1]]); - const endPoint = latLngToPoint([endLat, endLng], mapSize); + const boundingBox = getBoundingBoxFromExtent(extent, mapSize); - const width = Math.abs(endPoint.x - startPoint.x); - const height = Math.abs(endPoint.y - startPoint.y); - - if (!width || !height) { + if (!boundingBox.width || !boundingBox.height) { return; } - dispatch( - openLayerImageObjectFactoryModal({ - x: startPoint.x, - y: startPoint.y, - width, - height, - }), - ); + + dispatch(openLayerImageObjectFactoryModal(boundingBox)); }); return drawImageInteraction; diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..92a891c86a15ff5806d96c2fc6792868f3899f12 --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.test.ts @@ -0,0 +1,54 @@ +/* eslint-disable no-magic-numbers */ +import modalReducer from '@/redux/modal/modal.slice'; +import { MapSize } from '@/redux/map/map.types'; +import { + createStoreInstanceUsingSliceReducer, + ToolkitStoreWithSingleSlice, +} from '@/utils/createStoreInstanceUsingSliceReducer'; +import { ModalState } from '@/redux/modal/modal.types'; +import { DEFAULT_TILE_SIZE } from '@/constants/map'; +import { Collection, Feature } from 'ol'; +import getTransformImageInteraction from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction'; +import Transform from 'ol-ext/interaction/Transform'; +import { Geometry } from 'ol/geom'; + +jest.mock('../../../../../../../utils/map/latLngToPoint', () => ({ + latLngToPoint: jest.fn(latLng => ({ x: latLng[0], y: latLng[1] })), +})); + +describe('getTransformImageInteraction', () => { + let store = {} as ToolkitStoreWithSingleSlice<ModalState>; + let modelIdMock: number; + let layerIdMock: number; + let featuresCollectionMock: Collection<Feature<Geometry>>; + const mockDispatch = jest.fn(() => {}); + + let mapSize: MapSize; + + beforeEach(() => { + mapSize = { + width: 800, + height: 600, + minZoom: 1, + maxZoom: 9, + tileSize: DEFAULT_TILE_SIZE, + }; + store = createStoreInstanceUsingSliceReducer('modal', modalReducer); + store.dispatch = mockDispatch; + modelIdMock = 1; + layerIdMock = 1; + featuresCollectionMock = new Collection<Feature<Geometry>>(); + }); + + it('returns a Transform interaction', () => { + const transformInteraction = getTransformImageInteraction( + store.dispatch, + mapSize, + modelIdMock, + layerIdMock, + featuresCollectionMock, + [1000, 1000, 1000, 1000], + ); + expect(transformInteraction).toBeInstanceOf(Transform); + }); +}); diff --git a/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts new file mode 100644 index 0000000000000000000000000000000000000000..20041fa337ce2d8d6fe2c27215081cafbdfe4bfe --- /dev/null +++ b/src/components/Map/MapViewer/MapViewerVector/utils/shapes/layer/getTransformImageInteraction.ts @@ -0,0 +1,121 @@ +/* eslint-disable no-magic-numbers */ +import Polygon, { fromExtent } from 'ol/geom/Polygon'; +import { AppDispatch } from '@/redux/store'; +import Transform from 'ol-ext/interaction/Transform'; +import { Geometry } from 'ol/geom'; +import { Collection, Feature } from 'ol'; +import BaseEvent from 'ol/events/Event'; +import { updateLayerImageObject } from '@/redux/layers/layers.thunks'; +import { layerUpdateImage } from '@/redux/layers/layers.slice'; +import getBoundingBoxFromExtent from '@/components/Map/MapViewer/MapViewerVector/utils/shapes/coords/getBoundingBoxFromExtent'; +import { MapSize } from '@/redux/map/map.types'; +import { Extent } from 'ol/extent'; + +export default function getTransformImageInteraction( + dispatch: AppDispatch, + mapSize: MapSize, + modelId: number, + activeLayer: number, + featuresCollection: Collection<Feature<Geometry>>, + restrictionExtent: Extent, +): Transform { + const transform = new Transform({ + features: featuresCollection, + scale: true, + rotate: false, + stretch: false, + keepRectangle: true, + translate: true, + translateBBox: true, + noFlip: true, + }); + + transform.on('translating', (event: BaseEvent | Event) => { + const transformEvent = event as unknown as { feature: Feature }; + const { feature } = transformEvent; + const geometry = feature.getGeometry(); + if (geometry) { + const extent = geometry.getExtent(); + const [minX, minY, maxX, maxY] = extent; + + const width = maxX - minX; + const height = maxY - minY; + + const correctedMinX = Math.min( + restrictionExtent[2] - width, + Math.max(restrictionExtent[0], minX), + ); + const correctedMinY = Math.min( + restrictionExtent[3] - height, + Math.max(restrictionExtent[1], minY), + ); + + const dx = correctedMinX - minX; + const dy = correctedMinY - minY; + + if (dx !== 0 || dy !== 0) { + geometry.translate(dx, dy); + geometry.changed(); + transform.setSelection(new Collection([feature])); + } + } + }); + + transform.on('scaling', (event: BaseEvent | Event) => { + const transformEvent = event as unknown as { feature: Feature }; + const { feature } = transformEvent; + const geometry = feature.getGeometry(); + + if (!geometry) { + return; + } + + const extent = geometry.getExtent(); + const [minX, minY, maxX, maxY] = extent; + const newMinX = Math.max(minX, restrictionExtent[0]); + const newMinY = Math.max(minY, restrictionExtent[1]); + const newMaxX = Math.min(maxX, restrictionExtent[2]); + const newMaxY = Math.min(maxY, restrictionExtent[3]); + const newExtent = [newMinX, newMinY, newMaxX, newMaxY]; + + const hasChanged = newExtent.some((value, index) => value !== extent[index]); + + if (hasChanged) { + const newGeometry = fromExtent(newExtent); + newGeometry.scale(1, -1); + feature.setGeometry(newGeometry); + transform.setSelection(new Collection([feature])); + } + }); + + transform.on(['scaleend', 'translateend'], async (event: BaseEvent | Event): Promise<void> => { + const transformEvent = event as unknown as { feature: Feature }; + const { feature } = transformEvent; + const setCoordinates = feature.get('setCoordinates'); + const getGlyphData = feature.get('getGlyphData'); + const reset = feature.get('reset'); + const geometry = feature.getGeometry(); + if (geometry && getGlyphData instanceof Function) { + const glyphData = getGlyphData(); + try { + const boundingBox = getBoundingBoxFromExtent(geometry.getExtent(), mapSize); + const layerImage = await dispatch( + updateLayerImageObject({ modelId, layerId: activeLayer, ...glyphData, ...boundingBox }), + ).unwrap(); + if (layerImage) { + dispatch(layerUpdateImage({ modelId, layerId: activeLayer, layerImage })); + } + if (geometry instanceof Polygon && setCoordinates instanceof Function) { + setCoordinates(geometry.getCoordinates()); + geometry.changed(); + } + } catch { + if (reset instanceof Function) { + reset(); + } + } + } + }); + + return transform; +} diff --git a/src/redux/apiPath.ts b/src/redux/apiPath.ts index af6950d54d6226d48cbba1b00dd400ba27195294..3f062d67a43b1aedd46367fbe908093c4416f90e 100644 --- a/src/redux/apiPath.ts +++ b/src/redux/apiPath.ts @@ -67,6 +67,8 @@ export const apiPath = { `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`, addLayerImageObject: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/`, + updateLayerImageObject: (modelId: number, layerId: number, imageId: number): string => + `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}/images/${imageId}`, getLayer: (modelId: number, layerId: number): string => `projects/${PROJECT_ID}/maps/${modelId}/layers/${layerId}`, getGlyphImage: (glyphId: number): string => diff --git a/src/redux/layers/layers.reducers.ts b/src/redux/layers/layers.reducers.ts index 9e41f8ded97ed3a120bc25c17e06d5f55ae886f4..f5718ad4f23e8eb2898f418026564ab1c8c8c8fb 100644 --- a/src/redux/layers/layers.reducers.ts +++ b/src/redux/layers/layers.reducers.ts @@ -79,3 +79,19 @@ export const layerAddImageReducer = ( } layer.images[layerImage.id] = layerImage; }; + +export const layerUpdateImageReducer = ( + state: LayersState, + action: PayloadAction<{ modelId: number; layerId: number; layerImage: LayerImage }>, +): void => { + const { modelId, layerId, layerImage } = action.payload; + const { data } = state[modelId]; + if (!data) { + return; + } + const layer = data.layers.find(layerState => layerState.details.id === layerId); + if (!layer) { + return; + } + layer.images[layerImage.id] = layerImage; +}; diff --git a/src/redux/layers/layers.slice.ts b/src/redux/layers/layers.slice.ts index 9f78f0dd112b9e817107f37b9958b738dfab0ff4..9e4ea3cd29ddfae970fc02f46f95d416dd33b3ae 100644 --- a/src/redux/layers/layers.slice.ts +++ b/src/redux/layers/layers.slice.ts @@ -3,6 +3,7 @@ import { LAYERS_STATE_INITIAL_MOCK } from '@/redux/layers/layers.mock'; import { getLayersForModelReducer, layerAddImageReducer, + layerUpdateImageReducer, setActiveLayerReducer, setLayerVisibilityReducer, } from '@/redux/layers/layers.reducers'; @@ -14,12 +15,14 @@ export const layersSlice = createSlice({ setLayerVisibility: setLayerVisibilityReducer, setActiveLayer: setActiveLayerReducer, layerAddImage: layerAddImageReducer, + layerUpdateImage: layerUpdateImageReducer, }, extraReducers: builder => { getLayersForModelReducer(builder); }, }); -export const { setLayerVisibility, setActiveLayer, layerAddImage } = layersSlice.actions; +export const { setLayerVisibility, setActiveLayer, layerAddImage, layerUpdateImage } = + layersSlice.actions; export default layersSlice.reducer; diff --git a/src/redux/layers/layers.thunks.ts b/src/redux/layers/layers.thunks.ts index 354eb086e91b2bb6d3fa59a8c4c7093b9a5f4788..9c826c55cf02885614a01a75ee214caa91af9206 100644 --- a/src/redux/layers/layers.thunks.ts +++ b/src/redux/layers/layers.thunks.ts @@ -181,3 +181,43 @@ export const addLayerImageObject = createAsyncThunk< return Promise.reject(getError({ error })); } }); + +export const updateLayerImageObject = createAsyncThunk< + LayerImage | null, + { + modelId: number; + layerId: number; + id: number; + x: number; + y: number; + z: number; + width: number; + height: number; + glyph: number | null; + }, + ThunkConfig +>( + 'vectorMap/updateLayerImageObject', + async ({ modelId, layerId, id, x, y, z, width, height, glyph }) => { + try { + const { data } = await axiosInstanceNewAPI.put<LayerImage>( + apiPath.updateLayerImageObject(modelId, layerId, id), + { + x, + y, + z, + width, + height, + glyph, + }, + ); + const isDataValid = validateDataUsingZodSchema(data, layerImageSchema); + if (isDataValid) { + return data; + } + return null; + } catch (error) { + return Promise.reject(getError({ error })); + } + }, +); diff --git a/src/redux/mapEditTools/mapEditTools.constants.ts b/src/redux/mapEditTools/mapEditTools.constants.ts index 3f54d2b0e3720fe456fb0759663465d15b54c4b5..2524c4921fcde62474a6f01ab66be851fe22d909 100644 --- a/src/redux/mapEditTools/mapEditTools.constants.ts +++ b/src/redux/mapEditTools/mapEditTools.constants.ts @@ -1,3 +1,4 @@ export const MAP_EDIT_ACTIONS = { DRAW_IMAGE: 'DRAW_IMAGE', + TRANSFORM_IMAGE: 'TRANSFORM_IMAGE', } as const; diff --git a/src/shared/Icon/Icon.component.tsx b/src/shared/Icon/Icon.component.tsx index 784cfb0efe5c148cef4591a9f0753c809dab2a23..4d888fc27857a3659b614a2f777c9852b0e20901 100644 --- a/src/shared/Icon/Icon.component.tsx +++ b/src/shared/Icon/Icon.component.tsx @@ -19,6 +19,7 @@ import { PlusIcon } from '@/shared/Icon/Icons/PlusIcon'; import type { IconComponentType, IconTypes } from '@/types/iconTypes'; import { DownloadIcon } from '@/shared/Icon/Icons/DownloadIcon'; import { ImageIcon } from '@/shared/Icon/Icons/ImageIcon'; +import { ResizeImageIcon } from '@/shared/Icon/Icons/ResizeImageIcon'; import { LocationIcon } from './Icons/LocationIcon'; import { MaginfierZoomInIcon } from './Icons/MagnifierZoomIn'; import { MaginfierZoomOutIcon } from './Icons/MagnifierZoomOut'; @@ -61,6 +62,7 @@ const icons: Record<IconTypes, IconComponentType> = { user: UserIcon, 'manage-user': ManageUserIcon, image: ImageIcon, + 'resize-image': ResizeImageIcon, } as const; export const Icon = ({ name, className = '', ...rest }: IconProps): JSX.Element => { diff --git a/src/shared/Icon/Icons/ResizeImageIcon.tsx b/src/shared/Icon/Icons/ResizeImageIcon.tsx new file mode 100644 index 0000000000000000000000000000000000000000..71a0c3244cdd9b24e29ac8082cda7568df3f2a63 --- /dev/null +++ b/src/shared/Icon/Icons/ResizeImageIcon.tsx @@ -0,0 +1,19 @@ +interface ResizeImageIconProps { + className?: string; +} + +export const ResizeImageIcon = ({ className }: ResizeImageIconProps): JSX.Element => ( + <svg + width="24" + height="24" + viewBox="0 0 24 24" + fill="none" + className={className} + xmlns="http://www.w3.org/2000/svg" + > + <rect x="6" y="2" width="17" height="17" stroke="currentColor" strokeWidth="1.5" fill="none" /> + <rect x="1" y="14" width="9" height="9" stroke="currentColor" strokeWidth="1.5" fill="white" /> + <line x1="10" y1="14" x2="18" y2="7" stroke="currentColor" strokeWidth="1.5" /> + <polygon points="12,5 20,5 20,13" fill="currentColor" strokeWidth="1.5" /> + </svg> +); diff --git a/src/types/iconTypes.ts b/src/types/iconTypes.ts index 0ef11e99da00f9fde333a19968bd5fc7e46dd9b0..469b7699a5d3afc72da33a27ad6959ae8cbc7281 100644 --- a/src/types/iconTypes.ts +++ b/src/types/iconTypes.ts @@ -25,6 +25,7 @@ export type IconTypes = | 'manage-user' | 'download' | 'question' - | 'image'; + | 'image' + | 'resize-image'; export type IconComponentType = ({ className }: { className: string }) => JSX.Element;