diff --git a/docs/_config.yml b/docs/_config.yml index 6d588c25..f4904335 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -5,4 +5,4 @@ sass: style: compressed aux_links: "SLDReader on GitHub": - - "//github.com/nieuwlandgeo/sldreader" + - "//github.com/letsdev/sldreader" diff --git a/docs/assets/sldreader.js b/docs/assets/sldreader.js index 6e0cf095..555805c0 100644 --- a/docs/assets/sldreader.js +++ b/docs/assets/sldreader.js @@ -2688,12 +2688,11 @@ return [[p2$1[0], p2$1[1], calculateAngle(p1$1, p2$1, options.invertY)]]; } - var totalLength = geometry.getLength(); var gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. // Measure along line to place the next point. // Can start at a nonzero value if initialGap is used. - var nextPointMeasure = options.initialGap || 0.0; + var nextPointMeasure = gapSize / 2; var pointIndex = 0; var currentSegmentStart = [].concat( coords[0] ); var currentSegmentEnd = [].concat( coords[1] ); @@ -2704,13 +2703,31 @@ var splitPoints = []; // Keep adding points until the next point measure lies beyond the line length. - while (nextPointMeasure <= totalLength) { + while (true) { var currentSegmentLength = calculatePointsDistance( currentSegmentStart, currentSegmentEnd ); - if (cumulativeMeasure + currentSegmentLength < nextPointMeasure) { - // If the current segment is too short to reach the next point, go to the next segment. + // If the next point exceeds the line length (minus half the gapsize because we hook the image in the center, not the beginning), + // we go to the next segment. + if (cumulativeMeasure + currentSegmentLength < nextPointMeasure + gapSize / 2) { + + const splitPointCoords = calculateSplitPointCoords( + currentSegmentEnd, + currentSegmentStart, + gapSize / 2 + ); + const angle = calculateAngle( + currentSegmentStart, + currentSegmentEnd, + options.invertY + ); + if (!options.extent + || extent.containsCoordinate(options.extent, splitPointCoords)) { + splitPointCoords.push(angle); + splitPoints.push(splitPointCoords); + } + if (pointIndex === coords.length - 2) { // Stop if there is no next segment to process. break; @@ -2720,7 +2737,8 @@ currentSegmentEnd[0] = coords[pointIndex + 2][0]; currentSegmentEnd[1] = coords[pointIndex + 2][1]; pointIndex += 1; - cumulativeMeasure += currentSegmentLength; + cumulativeMeasure = 0; + nextPointMeasure = gapSize / 2 } else { // Next point lies on the current segment. // Calculate its position and increase next point measure by gap size. diff --git a/package-lock.json b/package-lock.json index 9f98ebee..d6844c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,17 @@ { - "name": "@nieuwlandgeo/sldreader", - "version": "0.4.3", + "name": "@disy/sldreader", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@nieuwlandgeo/sldreader", - "version": "0.4.3", + "name": "@disy/sldreader", + "version": "0.9.0", "license": "ISC", "devDependencies": { + "@babel/core": "^7.28.4", + "@babel/preset-env": "^7.28.3", + "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-buble": "^1.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "babel-preset-es2015": "^6.24.1", @@ -28,24 +31,1742 @@ "karma-rollup-preprocessor": "^7.0.8", "mocha": "^10.2.0", "npm-run-all": "^4.1.5", - "ol": "^7.2.2", + "ol": "10.7.0", "prettier": "^2.8.4", "rollup": "^3.17.2" }, "peerDependencies": { - "ol": ">= 5.3.0" + "ol": ">= 5.3.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/compat-data/-/compat-data-7.28.4.tgz", + "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/core/-/core-7.28.4.tgz", + "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", + "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.5", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", + "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "debug": "^4.4.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.10" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", + "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.4" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", + "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.28.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", + "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", + "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", + "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.3", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", + "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", + "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", + "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", + "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", + "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, - "bin": { - "parser": "bin/babel-parser.js" + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.27.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.28.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/preset-env/-/preset-env-7.28.3.tgz", + "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.28.0", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.0", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.28.3", + "@babel/plugin-transform-classes": "^7.28.3", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.28.0", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.28.0", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.28.3", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "core-js-compat": "^3.43.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/traverse/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/types": { + "version": "7.28.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, "node_modules/@colors/colors": { @@ -174,6 +1895,56 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://npm.letsdev.de/repository/npm-group/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://npm.letsdev.de/repository/npm-group/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://npm.letsdev.de/repository/npm-group/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://npm.letsdev.de/repository/npm-group/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, "node_modules/@jsdoc/salty": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@jsdoc/salty/-/salty-0.2.1.tgz", @@ -270,6 +2041,33 @@ "integrity": "sha512-oXZOc+aePd0FnhTWk15pyqK+Do87n0TyLV1nxdEougE95X/WXWDqmQobfhgnSY7QsWn5euZUWuDVeTQvoQ5VNw==", "dev": true }, + "node_modules/@rollup/plugin-babel": { + "version": "6.0.4", + "resolved": "https://npm.letsdev.de/repository/npm-group/@rollup/plugin-babel/-/plugin-babel-6.0.4.tgz", + "integrity": "sha512-YF7Y52kFdFT/xVSuVdjkV5ZdX/3YtmX0QulG+x0taQOtJdHYzVU61aSSkAgVJ7NOv6qPkIYiJSgSWWN/DM5sGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.18.6", + "@rollup/pluginutils": "^5.0.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "@types/babel__core": "^7.1.9", + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "@types/babel__core": { + "optional": true + }, + "rollup": { + "optional": true + } + } + }, "node_modules/@rollup/plugin-buble": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@rollup/plugin-buble/-/plugin-buble-1.0.2.tgz", @@ -838,6 +2636,48 @@ "babel-runtime": "^6.22.0" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.14", + "resolved": "https://npm.letsdev.de/repository/npm-group/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", + "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.7", + "@babel/helper-define-polyfill-provider": "^0.6.5", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.5", + "resolved": "https://npm.letsdev.de/repository/npm-group/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", + "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/babel-plugin-syntax-async-functions": { "version": "6.13.0", "resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz", @@ -1310,6 +3150,16 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.7", + "resolved": "https://npm.letsdev.de/repository/npm-group/baseline-browser-mapping/-/baseline-browser-mapping-2.8.7.tgz", + "integrity": "sha512-bxxN2M3a4d1CRoQC//IqsR5XrLh0IJ8TCv2x6Y9N0nckNz/rTjZB3//GGscZziZOxmjP55rzxg/ze7usFI9FqQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -1616,6 +3466,40 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.26.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/bs-recipes": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", @@ -1718,6 +3602,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/caniuse-lite": { + "version": "1.0.30001745", + "resolved": "https://npm.letsdev.de/repository/npm-group/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, "node_modules/catharsis": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", @@ -2024,6 +3929,13 @@ "node": ">= 0.6" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", @@ -2044,6 +3956,20 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/core-js-compat": { + "version": "3.45.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/core-js-compat/-/core-js-compat-3.45.1.tgz", + "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -2375,6 +4301,13 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", "dev": true }, + "node_modules/electron-to-chromium": { + "version": "1.5.224", + "resolved": "https://npm.letsdev.de/repository/npm-group/electron-to-chromium/-/electron-to-chromium-1.5.224.tgz", + "integrity": "sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==", + "dev": true, + "license": "ISC" + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -2600,10 +4533,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://npm.letsdev.de/repository/npm-group/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -3401,10 +5335,14 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -3433,6 +5371,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/geotiff": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/geotiff/-/geotiff-2.0.7.tgz", @@ -3693,6 +5641,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://npm.letsdev.de/repository/npm-group/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/he": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", @@ -3949,12 +5910,16 @@ } }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.16.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", "dev": true, + "license": "MIT", "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4707,6 +6672,13 @@ "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", "dev": true }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://npm.letsdev.de/repository/npm-group/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.isfinite": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", @@ -4809,6 +6781,16 @@ "get-func-name": "^2.0.0" } }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -5348,6 +7330,13 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "node_modules/node-releases": { + "version": "2.0.21", + "resolved": "https://npm.letsdev.de/repository/npm-group/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "dev": true, + "license": "MIT" + }, "node_modules/normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5742,6 +7731,13 @@ "pbf": "bin/pbf" } }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -6183,18 +8179,22 @@ } }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.10", + "resolved": "https://npm.letsdev.de/repository/npm-group/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, + "license": "MIT", "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -6353,10 +8353,11 @@ "dev": true }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } @@ -7360,6 +9361,37 @@ "node": ">= 0.8" } }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://npm.letsdev.de/repository/npm-group/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -7634,6 +9666,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://npm.letsdev.de/repository/npm-group/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yargs": { "version": "17.5.1", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.5.1.tgz", diff --git a/package.json b/package.json index 60bd6f06..b6c7ca1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@nieuwlandgeo/sldreader", - "version": "0.4.3", + "name": "@disy/sldreader", + "version": "0.9.0", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ @@ -27,7 +27,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/NieuwlandGeo/SLDReader.git" + "url": "git+https://github.com/letsdev/SLDReader.git" }, "author": "Allart Kooiman", "contributors": [ @@ -35,6 +35,9 @@ ], "license": "ISC", "devDependencies": { + "@babel/core": "^7.28.4", + "@babel/preset-env": "^7.28.3", + "@rollup/plugin-babel": "^6.0.4", "@rollup/plugin-buble": "^1.0.2", "@rollup/plugin-node-resolve": "^15.0.1", "babel-preset-es2015": "^6.24.1", @@ -54,7 +57,7 @@ "karma-rollup-preprocessor": "^7.0.8", "mocha": "^10.2.0", "npm-run-all": "^4.1.5", - "ol": "^7.2.2", + "ol": "10.7.0", "prettier": "^2.8.4", "rollup": "^3.17.2" }, diff --git a/rollup.config.mjs b/rollup.config.mjs index 52b29b87..fb11a842 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -1,4 +1,38 @@ -import buble from '@rollup/plugin-buble'; +// import buble from '@rollup/plugin-buble'; +// import { nodeResolve } from '@rollup/plugin-node-resolve'; +// +// export default { +// input: 'src/index.js', +// external: moduleId => moduleId.indexOf('ol') === 0, +// output: { +// file: 'dist/sldreader.js', +// format: 'umd', +// name: 'SLDReader', +// globals: { +// 'ol/style': 'ol.style', +// 'ol/render': 'ol.render', +// 'ol/extent': 'ol.extent', +// 'ol/geom': 'ol.geom', +// 'ol/has': 'ol.has', +// }, +// }, +// plugins: [ +// buble({ +// objectAssign: true, +// transforms: { +// asyncAwait: false, +// forOf: false, // Disable for...of transformation +// dangerousForOf: false, // Disable dangerous for...of transformation +// modules: false, // Keep ES6 modules +// }, +// }), +// nodeResolve(), +// ], +// }; + + + +import babel from '@rollup/plugin-babel'; import { nodeResolve } from '@rollup/plugin-node-resolve'; export default { @@ -16,5 +50,25 @@ export default { 'ol/has': 'ol.has', }, }, - plugins: [buble({ objectAssign: true }), nodeResolve()], + plugins: [ + babel({ + babelHelpers: 'bundled', + // Keep output close to source for readability: modern target, avoid generator transforms. + presets: [ + [ + '@babel/preset-env', + { + targets: { esmodules: true }, + bugfixes: true, + modules: false, + exclude: [ + 'transform-async-to-generator', + 'transform-regenerator', + ], + }, + ], + ], + }), + nodeResolve() + ], }; diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index 15d808a5..ac9108a1 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -2,37 +2,11 @@ import { containsCoordinate } from 'ol/extent'; import { PLACEMENT_FIRSTPOINT, PLACEMENT_LASTPOINT } from '../constants'; -function calculatePointsDistance(coord1, coord2) { - const dx = coord1[0] - coord2[0]; - const dy = coord1[1] - coord2[1]; - return Math.sqrt(dx * dx + dy * dy); -} - -function calculateSplitPointCoords(startCoord, endCoord, distanceFromStart) { - const distanceBetweenNodes = calculatePointsDistance(startCoord, endCoord); - const d = distanceFromStart / distanceBetweenNodes; - const x = startCoord[0] + (endCoord[0] - startCoord[0]) * d; - const y = startCoord[1] + (endCoord[1] - startCoord[1]) * d; - return [x, y]; -} - +// eslint-disable-next-line import/prefer-default-export /** - * Calculate the angle of a vector in radians clockwise from the positive x-axis. - * Example: (0,0) -> (1,1) --> -pi/4 radians. - * @private - * @param {Array} p1 Start of the line segment as [x,y]. - * @param {Array} p2 End of the line segment as [x,y]. - * @param {boolean} invertY If true, calculate with Y-axis pointing downwards. - * @returns {number} Angle in radians, clockwise from the positive x-axis. + * Creates a list of anchor points for images that will be rendered as a geometry line. Each point will be the center of + * such an image. The returned "splitPoints" include coordinates, angle, and in certain cases the length of the segment the point is on. */ -function calculateAngle(p1, p2, invertY) { - const dX = p2[0] - p1[0]; - const dY = p2[1] - p1[1]; - const angle = -Math.atan2(invertY ? -dY : dY, dX); - return angle; -} - -// eslint-disable-next-line import/prefer-default-export export function splitLineString(geometry, graphicSpacing, options = {}) { const coords = geometry.getCoordinates(); @@ -41,19 +15,16 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { if (coords.length === 0) { return []; } - // LineString containing only one point. if (coords.length === 1) { - return [[...coords[0], 0]]; + return [(coords[0]).concat([0])]; } - // Handle first point placement case. if (options.placement === PLACEMENT_FIRSTPOINT) { const p1 = coords[0]; const p2 = coords[1]; return [[p1[0], p1[1], calculateAngle(p1, p2, options.invertY)]]; } - // Handle last point placement case. if (options.placement === PLACEMENT_LASTPOINT) { const p1 = coords[coords.length - 2]; @@ -61,29 +32,67 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { return [[p2[0], p2[1], calculateAngle(p1, p2, options.invertY)]]; } - const totalLength = geometry.getLength(); const gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. - // Measure along line to place the next point. - // Can start at a nonzero value if initialGap is used. - let nextPointMeasure = options.initialGap || 0.0; let pointIndex = 0; - const currentSegmentStart = [...coords[0]]; - const currentSegmentEnd = [...coords[1]]; + const currentSegmentStart = [].concat(coords[0]); + const currentSegmentEnd = [].concat(coords[1]); - // Cumulative measure of the line where each segment's length is added in succession. - let cumulativeMeasure = 0; + let splitPointsOnThisSegment = 0; const splitPoints = []; - // Keep adding points until the next point measure lies beyond the line length. - while (nextPointMeasure <= totalLength) { + while (true) { const currentSegmentLength = calculatePointsDistance( currentSegmentStart, - currentSegmentEnd + currentSegmentEnd, ); - if (cumulativeMeasure + currentSegmentLength < nextPointMeasure) { - // If the current segment is too short to reach the next point, go to the next segment. + + let distanceFromStart; + + // If the next split point creates a line that is longer than the segment, it will be the last one on the segment. + // May also be the only split point. + if ((splitPointsOnThisSegment + 1) * gapSize >= currentSegmentLength) { + + if (splitPointsOnThisSegment === 0) { + // We put the first split point at the center of the first image, so half the gapsize away from the start. + distanceFromStart = 0.5 * gapSize; + } else { + // We put the last split point at the center of the last image, so half the gapsize away from the end. + distanceFromStart = currentSegmentLength - (0.5 * gapSize); + } + + const splitPointCoords = calculateSplitPointCoords({ + startCoord: currentSegmentStart, + endCoord: currentSegmentEnd, + distanceFromStart: distanceFromStart, + graphicWidth: gapSize, + }); + const angle = calculateAngle( + currentSegmentStart, + currentSegmentEnd, + options.invertY, + ); + // Only return split points that will be rendered (are in extent). + if (!options.extent + || containsCoordinate(options.extent, splitPointCoords)) { + const splitPoint = { + splitPointCoords: splitPointCoords, + angle: angle, + startingGeometryCoordIndex: pointIndex, + }; + /* + * If this is the only split point on this segment, we also add the current segment length. This might be used to + * calculate the correct image width in the rendering loop that is calling this function, in case the image is + * wider than the whole segment. + */ + if (splitPointsOnThisSegment === 0) { + splitPoint.segmentLength = currentSegmentLength; + splitPoint.isOnlySpOnThisSegment = true; + } + splitPoints.push(splitPoint); + } + if (pointIndex === coords.length - 2) { // Stop if there is no next segment to process. break; @@ -93,31 +102,318 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { currentSegmentEnd[0] = coords[pointIndex + 2][0]; currentSegmentEnd[1] = coords[pointIndex + 2][1]; pointIndex += 1; - cumulativeMeasure += currentSegmentLength; - } else { - // Next point lies on the current segment. - // Calculate its position and increase next point measure by gap size. - const distanceFromSegmentStart = nextPointMeasure - cumulativeMeasure; - const splitPointCoords = calculateSplitPointCoords( - currentSegmentStart, - currentSegmentEnd, - distanceFromSegmentStart - ); + splitPointsOnThisSegment = 0; + } else { // The next split point does *not* exceed the segment length, so it won't be the last one. + + if (splitPointsOnThisSegment === 0) { + // We put the first split point at the center of the first image, so half the gapsize away from the start. + distanceFromStart = 0.5 * gapSize; + } else { + // We put all other split points (except the last one, but that's handled in the `if` branch) + // exactly "one image width apart" (== gapSize) from each other. Since `distanceFromStart` is the total length of the + // line computed thus far, we include the half gapsize of the first point, hence `+ 0.5`. + distanceFromStart = (splitPointsOnThisSegment + 0.5) * gapSize; + } + + // We don't need to provide the graphic width here, since it can never be longer than the segment once we're in the `else` block. + const splitPointCoords = calculateSplitPointCoords({ + startCoord: currentSegmentStart, + endCoord: currentSegmentEnd, + distanceFromStart: distanceFromStart, + }); const angle = calculateAngle( currentSegmentStart, currentSegmentEnd, - options.invertY + options.invertY, ); + // Only return split points that will be rendered (are in extent). if ( !options.extent || containsCoordinate(options.extent, splitPointCoords) ) { - splitPointCoords.push(angle); - splitPoints.push(splitPointCoords); + // We don't add the segment length here, since the graphic is for sure not wider than the segment once we're in this else block. + splitPoints.push({ + splitPointCoords: splitPointCoords, + angle: angle, + startingGeometryCoordIndex: pointIndex, + }); } - nextPointMeasure += gapSize; + splitPointsOnThisSegment++; } } return splitPoints; } + +export function getGapCloserPoints(options) { + const coordOnFirstLine = options.coordOnFirstLine; + const intersectCoord = options.intersectCoord; + const coordOnSecondLine = options.coordOnSecondLine; + const mirrorOffset = options.mirrorOffset; + + const isRightTurn = getIsRightTurn( + coordOnFirstLine, + intersectCoord, + coordOnSecondLine, + ); + + const mirroredCoords = getMirroredCoords( + coordOnFirstLine, + intersectCoord, + coordOnSecondLine, + !isRightTurn, + mirrorOffset, + ); + + const angleAtMirroredIntersect = angleInRadiansAtB( + mirroredCoords.coords2, + mirroredCoords.intersect, + mirroredCoords.coords3, + ); + + const returnData = { + forwardPoint: null, + backwardPoint: null, + isRightTurn: isRightTurn, + }; + + // Forwards direction + const angleFwd = calculateAngle(coordOnFirstLine, intersectCoord, true); + const cutLength = calculatePointsDistance( + mirroredCoords.coords2, + mirroredCoords.intersect, + ); + returnData.forwardPoint = { + intersectCoords: intersectCoord, + angle: angleFwd, + cutLength: cutLength, + cutAngle: angleAtMirroredIntersect / 2, // We only need half for each of the two gap filling parts. + isRightTurn: isRightTurn, + isFirst: true, + }; + + // Backwards direction + const angleBwd = calculateAngle(intersectCoord, coordOnSecondLine, true); + returnData.backwardPoint = { + intersectCoords: intersectCoord, + mirroredIntersect: mirroredCoords.intersect, + angle: angleBwd, + cutLength: calculatePointsDistance( + mirroredCoords.intersect, + mirroredCoords.coords3, + ), + cutAngle: angleAtMirroredIntersect / 2, // This is the second half. + isRightTurn: isRightTurn, + isFirst: false, + }; + + return returnData; +} + +export function angleInRadiansAtB(a, b, c) { + // Vectors AB and CB + const ab = [a[0] - b[0], a[1] - b[1]]; + const cb = [c[0] - b[0], c[1] - b[1]]; + + // Dot product and magnitudes + const dot = ab[0] * cb[0] + ab[1] * cb[1]; + const magAB = Math.hypot(...ab); + const magCB = Math.hypot(...cb); + + // Clamp value for safety against floating point errors + const cosTheta = Math.min(Math.max(dot / (magAB * magCB), -1), 1); + + // Return angle in radians + return Math.acos(cosTheta); +} + +/** + * Euclidean distance between two points ([x, y]). + */ +export function calculatePointsDistance(coord1, coord2) { + const dx = coord1[0] - coord2[0]; + const dy = coord1[1] - coord2[1]; + return Math.sqrt(dx * dx + dy * dy); +} + +export function getMirroredCoords( + coordOnFirstLine, + intersectCoord, + coordOnSecondLine, + useRight, + offset, +) { + const mirroredCoords1 = getSidePointFromLine(coordOnFirstLine, intersectCoord, offset, useRight); + const mirroredCoords2 = getSidePointFromLine(intersectCoord, coordOnFirstLine, offset, !useRight); + const mirroredCoords3 = getSidePointFromLine(intersectCoord, coordOnSecondLine, offset, useRight); + const mirroredCoords4 = getSidePointFromLine(coordOnSecondLine, intersectCoord, offset, !useRight); + + const mirroredIntersect = getLineIntersect([ + mirroredCoords1, + mirroredCoords2, + ], [ + mirroredCoords4, + mirroredCoords3, + ]); + + return { + coords1: mirroredCoords1, + coords2: mirroredCoords2, + coords3: mirroredCoords3, + coords4: mirroredCoords4, + intersect: mirroredIntersect, + }; +} + +/* Mirror D on C to create E, and then return true if E is contained in the triangle A-B-D */ +export function isEInsideTriangleAfterMirror(A, B, C, D) { + // Mirror D on C to get E + const E = [ + 2 * C[0] - D[0], + 2 * C[1] - D[1], + ]; + + // Helper function to compute the "sign" of area (cross product) + function sign(p1, p2, p3) { + return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1]); + } + + const d1 = sign(E, A, B); + const d2 = sign(E, B, D); + const d3 = sign(E, D, A); + + const hasNeg = (d1 < 0) || (d2 < 0) || (d3 < 0); + const hasPos = (d1 > 0) || (d2 > 0) || (d3 > 0); + + // E is inside the triangle if it is not strictly on both sides + return !(hasNeg && hasPos); +} + +export function getIsRightTurn(a, b, c) { + const mirroredCoords1Right = getSidePointFromLine(a, b, 1, true); + const mirroredCoords1Left = getSidePointFromLine(a, b, 1, false); + const d1 = calculatePointsDistance(mirroredCoords1Right, c); + const d2 = calculatePointsDistance(mirroredCoords1Left, c); + return d2 > d1; +} + +function getSidePointFromLine(point1, point2, distance, getRightSide) { + // Calculate the direction vector from point1 to point2 + const dx = point2[0] - point1[0]; + const dy = point2[1] - point1[1]; + + // Handle degenerate case where points are the same + if (dx === 0 && dy === 0) { + return [point1[0], point1[1]]; // Return original point if no direction + } + + // Calculate perpendicular vector (rotate 90 degrees) + // For vector (dx, dy), perpendicular vectors are (-dy, dx) and (dy, -dx) + let perpDx = -dy; + let perpDy = dx; + + // Normalize to unit vector + const length = Math.hypot(perpDx, perpDy); + perpDx /= length; + perpDy /= length; + + const candidateOne = [ + point1[0] + perpDx * distance, + point1[1] + perpDy * distance, + ]; + const candidateTwo = [ + point1[0] + (perpDx * -1) * distance, + point1[1] + (perpDy * -1) * distance, + ]; + + let candidateOneIsOnTheRight; + if (point2[0] > point1[0] + && point2[1] === point1[1]) { + candidateOneIsOnTheRight = candidateOne[1] > candidateTwo[1]; + } else if (point2[0] > point1[0] + && point2[1] > point1[1]) { + candidateOneIsOnTheRight = candidateOne[1] > candidateTwo[1]; + } else if (point2[0] === point1[0] + && point2[1] > point1[1]) { + candidateOneIsOnTheRight = candidateOne[0] < candidateTwo[0]; + } else if (point2[0] < point1[0] + && point2[1] > point1[1]) { + candidateOneIsOnTheRight = candidateOne[1] < candidateTwo[1]; + } else if (point2[0] < point1[0] + && point2[1] === point1[1]) { + candidateOneIsOnTheRight = candidateOne[1] < candidateTwo[1]; + } else if (point2[0] < point1[0] + && point2[1] < point1[1]) { + candidateOneIsOnTheRight = candidateOne[1] < candidateTwo[1]; + } else if (point2[0] === point1[0] + && point2[1] < point1[1]) { + candidateOneIsOnTheRight = candidateOne[0] > candidateTwo[0]; + } else if (point2[0] > point1[0] + && point2[1] < point1[1]) { + candidateOneIsOnTheRight = candidateOne[1] > candidateTwo[1]; + } + + return getRightSide + ? (candidateOneIsOnTheRight ? candidateOne : candidateTwo) + : (candidateOneIsOnTheRight ? candidateTwo : candidateOne); +} + +/** + * Calculate the angle of a vector in radians clockwise from the positive x-axis. + * Example: (0,0) -> (1,1) --> -pi/4 radians. + * @private + * @param {Array} p1 Start of the line segment as [x,y]. + * @param {Array} p2 End of the line segment as [x,y]. + * @param {boolean} invertY If true, calculate with Y-axis pointing downwards. + * @returns {number} Angle in radians, clockwise from the positive x-axis. + */ +function calculateAngle(p1, p2, invertY) { + const dX = p2[0] - p1[0]; + const dY = p2[1] - p1[1]; + const angle = -Math.atan2(invertY ? -dY : dY, dX); + return angle; +} + +/** + * Calculates a point along the line between the provided startCoord and endCoord. The distance between the + * startCoord and the resulting point will be exactly `distanceFromStart`. The resulting point will never lie + * outside of the provided segment. If a graphicWidth is provided, and this width is larger than the segment + * length, the point will be placed exactly in the middle of the segment. + */ +function calculateSplitPointCoords(options) { + const startCoord = options.startCoord; + const endCoord = options.endCoord; + const distanceFromStart = options.distanceFromStart; + + const distanceBetweenNodes = calculatePointsDistance(startCoord, endCoord); + let d = Math.max(Math.min(distanceFromStart / distanceBetweenNodes, 1), 0); // clamp this between 0 and 1 to prevent points outside of the segment + if (!!options.graphicWidth && options.graphicWidth > distanceBetweenNodes) { + d = 0.5; + } + const x = startCoord[0] + (endCoord[0] - startCoord[0]) * d; + const y = startCoord[1] + (endCoord[1] - startCoord[1]) * d; + return [x, y]; +} + +function getLineIntersect(line1Coords, line2Coords) { + const x1 = line1Coords[0][0]; + const y1 = line1Coords[0][1]; + const x2 = line1Coords[1][0]; + const y2 = line1Coords[1][1]; + const x3 = line2Coords[0][0]; + const y3 = line2Coords[0][1]; + const x4 = line2Coords[1][0]; + const y4 = line2Coords[1][1]; + + // https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection#Given_two_points_on_each_line + const denom = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4); + const px = ((x1 * y2 - y1 * x2) * (x3 - x4) - + (x1 - x2) * (x3 * y4 - y3 * x4)) / denom; + const py = ((x1 * y2 - y1 * x2) * (y3 - y4) - + (y1 - y2) * (x3 * y4 - y3 * x4)) / denom; + + return [ + px, + py, + ]; +} diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 7e80100e..a460b30a 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -1,7 +1,6 @@ -import { Style } from 'ol/style'; +import { Style, Icon } from 'ol/style'; import { toContext } from 'ol/render'; import { Point, LineString } from 'ol/geom'; - import { DEFAULT_MARK_SIZE, DEFAULT_EXTERNALGRAPHIC_SIZE, @@ -12,14 +11,62 @@ import { import evaluate from '../olEvaluator'; import getPointStyle from './pointStyle'; import { calculateGraphicSpacing, getInitialGapSize } from './styleUtils'; -import { splitLineString } from './geometryCalcs'; +import { + splitLineString, + getGapCloserPoints, + getIsRightTurn, + getMirroredCoords, + angleInRadiansAtB, + calculatePointsDistance, +} from './geometryCalcs'; // A flag to prevent multiple renderer patches. let rendererPatched = false; + +// Global counter to track render sessions (to avoid deferred renderings being rendered after zoom, resulting in duplications and incorrect placing) +let currentRenderSessionByFeatureOlUid = new Map(); + +/** + * Object containing maps to cache clipped image data as base64 strings. + * Separate maps are used for different types of turns/segments. + * @private + */ +const clipInfoHashToBase64 = { + rightTurn: new Map(), + leftTurn: new Map(), + straight: new Map(), +}; +const imageSourceHashToIsFullImg = new Map(); +const USE_CACHING = true; + +const TURN_DIRECTION_CONFIG = { + right: { + clipCacheKey: 'rightTurn', + firstGapFlag: 'isFirstOfRightTurn', + secondGapFlag: 'isSecondOfRightTurn', + nextSegmentFlag: 'isFirstAfterRightTurn', + getEdgeY: () => 0, + getOppositeY: canvasHeight => canvasHeight, + angledYDirection: 1, + }, + left: { + clipCacheKey: 'leftTurn', + firstGapFlag: 'isFirstOfLeftTurn', + secondGapFlag: 'isSecondOfLeftTurn', + nextSegmentFlag: 'isFirstAfterLeftTurn', + getEdgeY: canvasHeight => canvasHeight, + getOppositeY: () => 0, + angledYDirection: -1, + }, +}; + +// Used to quickly check if canvasses are empty +let emptyCanvasSrc = null; + function patchRenderer(renderer) { - if (rendererPatched) { - return; - } + // if (rendererPatched) { + // return; + // } // Add setImageStyle2 function that does the same as setImageStyle, except that it sets rotation // to a given value instead of taking it from imageStyle.getRotation(). @@ -52,30 +99,46 @@ function patchRenderer(renderer) { * @param {number} pixelRatio Ratio of device pixels to css pixels. * @returns {void} */ -function renderStrokeMarks( - render, +async function renderStrokeMarks( + renderContext, pixelCoords, graphicSpacing, pointStyle, pixelRatio, - options + options, + geometryType, + feature, ) { if (!pixelCoords) { return; } + // This will be used when rendering images that have just loaded, to make sure the render is still current, will be aborted otherwise. + currentRenderSessionByFeatureOlUid.set( + feature.ol_uid, + currentRenderSessionByFeatureOlUid.has(feature.ol_uid) + ? currentRenderSessionByFeatureOlUid.get(feature.ol_uid) + 1 + : 1, + ); + const thisRenderSession = currentRenderSessionByFeatureOlUid.get(feature.ol_uid); + + // We use the context as param and create the render object here, because we need to create deep copies later. + const render = toContext(renderContext); + // The first element of the first pixelCoords entry should be a number (x-coordinate of first point). // If it's an array instead, then we're dealing with a multiline or (multi)polygon. // In that case, recursively call renderStrokeMarks for each child coordinate array. if (Array.isArray(pixelCoords[0][0])) { - pixelCoords.forEach(pixelCoordsChildArray => { + pixelCoords.forEach(function (pixelCoordsChildArray) { renderStrokeMarks( - render, + renderContext, pixelCoordsChildArray, graphicSpacing, pointStyle, pixelRatio, - options + options, + geometryType, + feature, ); }); return; @@ -87,31 +150,1414 @@ function renderStrokeMarks( } // Don't render anything when the pointStyle has no image. - const image = pointStyle.getImage(); - if (!image) { + const ogImage = pointStyle.getImage(); + if (!ogImage) { + return; + } + + if (!ogImage.iconImage_) { return; } + let ogImageWidth = null; + let ogImageHeight = null; + if (ogImage.getSize()) { + ogImageWidth = ogImage.getSize()[0]; + ogImageHeight = ogImage.getSize()[1]; + } + + const gapSize = graphicSpacing * pixelRatio; + const isFullImg = await getIsFullImg({ + image: ogImage, + imageWidth: ogImageWidth, + imageHeight: ogImageHeight, + pixelRatio, + }); + const splitPoints = splitLineString( new LineString(pixelCoords), - graphicSpacing * pixelRatio, + gapSize, { invertY: true, // Pixel y-coordinates increase downwards in screen space. extent: render.extent_, placement: options.placement, initialGap: options.initialGap, + graphicWidth: ogImageWidth, + }, + ); + + // These placement options don't work with / require our complex clipping logic below. Simply render those as in previous versions. + if ([PLACEMENT_FIRSTPOINT, PLACEMENT_LASTPOINT].includes(options.placement)) { + splitPoints.forEach(point => { + const splitPointAngle = ogImage.getRotation() + point[2]; + patchRenderer(render); + render.setImageStyle2(ogImage, splitPointAngle); + render.drawPoint(new Point([point[0] / pixelRatio, point[1] / pixelRatio])); + }); + + return; + } + + const pointsDataToRender = []; + let currentGeometryCoordIndex = null; + for (let i = 0; i < splitPoints.length; i++) { + const point = splitPoints[i]; + let customRender = render; + let image = ogImage; + let renderCoords = point.splitPointCoords; + + const isFirstOfGeometry = i === 0; + const isFirstOfSegment = currentGeometryCoordIndex !== point.startingGeometryCoordIndex; + if (isFirstOfSegment) { + currentGeometryCoordIndex = point.startingGeometryCoordIndex; } + const isRightTurn = !isFirstOfGeometry + && isFirstOfSegment + && getIsRightTurn( + pixelCoords[currentGeometryCoordIndex - 1], + pixelCoords[currentGeometryCoordIndex], + pixelCoords[currentGeometryCoordIndex + 1], + ); + const isLeftTurn = !isFirstOfGeometry + && isFirstOfSegment + && !isRightTurn; + const isRegularButShortened = !isRightTurn + && !isLeftTurn + && point.segmentLength !== null && point.segmentLength !== undefined; + + point.isRightTurn = isRightTurn; + point.isLeftTurn = isLeftTurn; + point.isFirstOfSegment = isFirstOfSegment; + point.isFirstOfGeometry = isFirstOfGeometry; + point.isRegularButShortened = isRegularButShortened; + + const newPointsDataToRender = await getNewPointsDataToRender({ + i, + point, + splitPoints, + pixelCoords, + geometryType, + currentGeometryCoordIndex, + pointsDataToRender, + ogImage, + ogImageWidth, + ogImageHeight, + pixelRatio, + gapSize, + image, + renderCoords, + customRender, + renderContext, + isRightTurn, + isLeftTurn, + isFirstOfGeometry, + isFirstOfSegment, + isRegularButShortened, + isFullImg, + }); + + newPointsDataToRender.forEach(it => { + it.fromSplitPoint = point; + }); + + pointsDataToRender.push(...newPointsDataToRender); + } + + pointsDataToRender + .filter(it => !it.ignore) + .forEach(pointDataToRender => { + renderPoint({ + image: pointDataToRender.image, + angle: pointDataToRender.angle, + coords: pointDataToRender.coords, + renderToUse: pointDataToRender.rendererToUse, + pixelRatio: pixelRatio, + renderSession: thisRenderSession, + feature, + }); + }); +} + +/** + * Dispatch turn handling to the full-image or half-image path. + * @param {Object} options Per-split-point rendering context. + * @returns {Promise>} Render data for the current split point. + */ +async function getNewPointsDataToRender(options) { + if (options.isFullImg) { + return getNewPointsDataToRenderForFullImg(options); + } + + return getNewPointsDataToRenderForHalfImg(options); +} + +/** + * Build render data for full images, where both turn directions use the shared + * turn pipeline. + * @param {Object} options Per-split-point rendering context. + * @returns {Promise>} Render data for the current split point. + */ +async function getNewPointsDataToRenderForFullImg(options) { + const { + isRightTurn, + isLeftTurn, + } = options; + let newPointsDataToRender; + + if (isRightTurn) { + newPointsDataToRender = await handleCurrentTurn(options, handleRightTurn); + } else if (isLeftTurn) { + newPointsDataToRender = await handleCurrentTurn(options, handleLeftTurn); + } else { + newPointsDataToRender = await getRegularOrUnchangedPointsData(options); + } + + await appendPolygonClosingGapFillRenderData(options, newPointsDataToRender, { + supportsClosingLeftTurn: true, + }); + + return newPointsDataToRender; +} + +/** + * Build render data for half images, preserving the dedicated left-turn path. + * @param {Object} options Per-split-point rendering context. + * @returns {Promise>} Render data for the current split point. + */ +async function getNewPointsDataToRenderForHalfImg(options) { + const { + i, + point, + splitPoints, + isRightTurn, + isLeftTurn, + isFirstOfSegment, + } = options; + const isFirstAfterLeftTurn = !isLeftTurn + && !isRightTurn + && !isFirstOfSegment + && i > 1 + && splitPoints[i - 1].isLeftTurn; + + point.isFirstAfterLeftTurn = isFirstAfterLeftTurn; + + let newPointsDataToRender; + if (isRightTurn) { + newPointsDataToRender = await handleCurrentTurn(options, handleRightTurn); + } else if (isLeftTurn) { + newPointsDataToRender = await handleHalfImageLeftTurn(options); + } else if (isFirstAfterLeftTurn) { + newPointsDataToRender = await handleHalfImageFirstAfterLeftTurn(options); + } else { + newPointsDataToRender = await getRegularOrUnchangedPointsData(options); + } + + await appendPolygonClosingGapFillRenderData(options, newPointsDataToRender, { + supportsClosingLeftTurn: false, + }); + + return newPointsDataToRender; +} + +/** + * Invoke a turn handler with the normalized option structure used by right and + * left turns. + * @param {Object} options Per-split-point rendering context. + * @param {Function} turnHandler Turn-specific handler to execute. + * @returns {Promise>} Render data for the turn. + */ +async function handleCurrentTurn(options, turnHandler) { + return turnHandler(getTurnHandlerOptions(options, { + splitPoint: options.point, + onlyDoGap: false, + involvedGeometryCoords: getInvolvedGeometryCoords( + options.pixelCoords, + options.currentGeometryCoordIndex, + ), + })); +} + +/** + * Choose between shortened-segment rendering and the unchanged render point. + * @param {Object} options Per-split-point rendering context. + * @returns {Promise>} Render data for the current split point. + */ +async function getRegularOrUnchangedPointsData(options) { + if (options.isRegularButShortened) { + return handleRegularButShortened(getRegularButShortenedOptions(options)); + } + + return [createUnchangedPointData(options)]; +} + +/** + * Build the shared option payload consumed by turn handlers. + * @param {Object} options Per-split-point rendering context. + * @param {Object} overrides Turn-specific overrides. + * @returns {Object} Normalized turn-handler options. + */ +function getTurnHandlerOptions(options, overrides = {}) { + return { + currentGeometryCoordIndex: options.currentGeometryCoordIndex, + pointsDataToRender: options.pointsDataToRender, + pImage: options.ogImage, + pImageWidth: options.ogImageWidth, + pImageHeight: options.ogImageHeight, + pRenderContext: options.renderContext, + ogImageWidth: options.ogImageWidth, + ogImageHeight: options.ogImageHeight, + gapSize: options.gapSize, + ...overrides, + }; +} + +/** + * Build the option payload for shortened straight segments. + * @param {Object} options Per-split-point rendering context. + * @returns {Object} Options for shortened straight rendering. + */ +function getRegularButShortenedOptions(options) { + return { + point: options.point, + gapSize: options.gapSize, + ogImageWidth: options.ogImageWidth, + ogImageHeight: options.ogImageHeight, + ogImage: options.ogImage, + pixelRatio: options.pixelRatio, + image: options.image, + renderCoords: options.renderCoords, + customRender: options.customRender, + currentGeometryCoordIndex: options.currentGeometryCoordIndex, + }; +} + +/** + * Return the geometry coordinates adjacent to the current turn vertex. + * @param {Array>} pixelCoords Geometry coordinates in pixel space. + * @param {number} geometryCoordIndex Index of the current vertex. + * @returns {Object} Incoming coord, vertex coord, and outgoing coord. + */ +function getInvolvedGeometryCoords(pixelCoords, geometryCoordIndex) { + return { + coordOnFirstLine: pixelCoords[geometryCoordIndex - 1], + intersectCoord: pixelCoords[geometryCoordIndex], + coordOnSecondLine: pixelCoords[geometryCoordIndex + 1], + }; +} + +/** + * Create render data for a split point that does not need special clipping. + * @param {Object} options Per-split-point rendering context. + * @returns {Object} Render data for the unchanged point. + */ +function createUnchangedPointData(options) { + return { + image: options.image, + angle: options.point.angle, + coords: options.renderCoords, + rendererToUse: options.customRender, + geometryCoordIndex: options.currentGeometryCoordIndex, + }; +} + +/** + * Detect the synthetic closing turn of a polygon ring and collect the geometry + * data needed to render its gap filler. + * @param {Object} options Per-split-point rendering context. + * @returns {?Object} Closing-turn context or `null` when not applicable. + */ +function getPolygonClosingContext(options) { + const { + i, + point, + splitPoints, + pixelCoords, + geometryType, + } = options; + const isPolygon = geometryType.includes('olygon'); + if (!isPolygon || i !== splitPoints.length - 1) { + return null; + } + + const hasAdditionalPixelCoord = point.startingGeometryCoordIndex === pixelCoords.length - 2; + if (!hasAdditionalPixelCoord) { + return null; + } + + const nextPixelIsClosingPoint = point.startingGeometryCoordIndex !== 0 + && pixelCoords[point.startingGeometryCoordIndex + 1][0] === pixelCoords[0][0] + && pixelCoords[point.startingGeometryCoordIndex + 1][1] === pixelCoords[0][1]; + if (!nextPixelIsClosingPoint) { + return null; + } + + const lastSplitPoint = point; + const firstSplitPoint = splitPoints[0]; + return { + firstSplitPoint, + endOfPolygonIsRightTurn: getIsRightTurn( + pixelCoords[lastSplitPoint.startingGeometryCoordIndex], + pixelCoords[lastSplitPoint.startingGeometryCoordIndex + 1], + pixelCoords[1], + ), + involvedGeometryCoords: { + coordOnFirstLine: pixelCoords[lastSplitPoint.startingGeometryCoordIndex], + intersectCoord: pixelCoords[0], + coordOnSecondLine: pixelCoords[1], + }, + }; +} + +/** + * Append polygon-closing gap-fill render data when the current split point is + * the last one on a polygon ring. + * @param {Object} options Per-split-point rendering context. + * @param {Array} newPointsDataToRender Render data collected so far. + * @param {Object} policy Flags controlling which closing turns are supported. + * @returns {Promise} + */ +async function appendPolygonClosingGapFillRenderData(options, newPointsDataToRender, policy) { + const polygonClosingGapFillRenderData = await getPolygonClosingGapFillRenderData(options, policy); + if (polygonClosingGapFillRenderData) { + newPointsDataToRender.push(...polygonClosingGapFillRenderData); + } +} + +/** + * Create the gap-filling render data for the implicit closing turn of a polygon + * ring when supported by the active rendering policy. + * @param {Object} options Per-split-point rendering context. + * @param {Object} policy Flags controlling which closing turns are supported. + * @returns {Promise>} Gap-fill render data or `null`. + */ +async function getPolygonClosingGapFillRenderData(options, policy) { + const polygonClosingContext = getPolygonClosingContext(options); + if (!polygonClosingContext) { + return null; + } + + if (!polygonClosingContext.endOfPolygonIsRightTurn && !policy.supportsClosingLeftTurn) { + return null; + } + + const turnHandler = polygonClosingContext.endOfPolygonIsRightTurn + ? handleRightTurn + : handleLeftTurn; + return turnHandler(getTurnHandlerOptions(options, { + currentGeometryCoordIndex: polygonClosingContext.firstSplitPoint.startingGeometryCoordIndex, + splitPoint: polygonClosingContext.firstSplitPoint, + onlyDoGap: true, + involvedGeometryCoords: polygonClosingContext.involvedGeometryCoords, + })); +} + +/** + * Detect whether an icon contains visible content in the lower half and should + * therefore use the full-image rendering path. + * @param {Object} options Image and sizing information. + * @returns {Promise} `true` when the image is treated as full. + */ +async function getIsFullImg(options) { + const { + image, + imageWidth, + imageHeight, + pixelRatio, + } = options; + if (!imageWidth || !imageHeight) { + return false; + } + + const imageSrc = image.iconImage_?.src_ || image.getSrc?.() || ''; + const cacheKey = `${imageSrc}|${imageWidth}x${imageHeight}`; + if (imageSourceHashToIsFullImg.has(cacheKey)) { + return imageSourceHashToIsFullImg.get(cacheKey); + } + + const imageElement = await getLoadedImageElement(image, pixelRatio); + if (!imageElement) { + imageSourceHashToIsFullImg.set(cacheKey, false); + return false; + } + + let isFullImg = false; + try { + const canvas = document.createElement('canvas'); + canvas.width = imageWidth; + canvas.height = imageHeight; + const ctx = canvas.getContext('2d'); + ctx.drawImage(imageElement, 0, 0, imageWidth, imageHeight); + + const bottomHalfStartY = Math.min(imageHeight, Math.floor(imageHeight / 2) + 1); + const bottomHalfHeight = imageHeight - bottomHalfStartY; + if (bottomHalfHeight > 0) { + const pixelData = ctx.getImageData(0, bottomHalfStartY, imageWidth, bottomHalfHeight).data; + for (let i = 3; i < pixelData.length; i += 4) { + if (pixelData[i] > 16) { + isFullImg = true; + break; + } + } + } + } catch (_) { + isFullImg = false; + } + + imageSourceHashToIsFullImg.set(cacheKey, isFullImg); + return isFullImg; +} + +/** + * Resolve the concrete image element used by an OpenLayers icon, waiting for it + * to load when necessary. + * @param {Icon} image OpenLayers icon style. + * @param {number} pixelRatio Current device pixel ratio. + * @returns {Promise} Loaded image element or `null`. + */ +function getLoadedImageElement(image, pixelRatio) { + const imageElement = image.getImage(pixelRatio) || image.iconImage_?.image_; + return new Promise(resolve => { + if (!imageElement) { + resolve(null); + return; + } + + if (typeof HTMLCanvasElement !== 'undefined' && imageElement instanceof HTMLCanvasElement) { + resolve(imageElement); + return; + } + + if (imageElement.complete) { + resolve(imageElement); + return; + } + + imageElement.onload = () => resolve(imageElement); + imageElement.onerror = () => resolve(null); + }); +} + +/** + * Look up the direction-specific clipping and flag configuration for a turn. + * @param {'right'|'left'} turnDirection Requested turn direction. + * @returns {Object} Direction-specific config object. + */ +function getTurnDirectionConfig(turnDirection) { + const turnDirectionConfig = TURN_DIRECTION_CONFIG[turnDirection]; + + if (!turnDirectionConfig) { + throw new Error(`Unsupported turn direction: ${turnDirection}`); + } + + return turnDirectionConfig; +} + +/** + * Thin wrapper that routes right turns into the shared turn implementation. + * @param {Object} options Turn-rendering options. + * @returns {Promise>} Render data for the turn. + */ +async function handleRightTurn(options) { + return handleTurn({ + ...options, + turnDirection: 'right', + }); +} + +/** + * Render the shared turn pipeline used by right turns and full-image left turns. + * @param {Object} options Turn-rendering options. + * @returns {Promise>} Render data for the turn. + */ +async function handleTurn(options) { + const currentGeometryCoordIndex = options.currentGeometryCoordIndex; + const pImage = options.pImage; + const pImageWidth = options.pImageWidth; + const pImageHeight = options.pImageHeight; + const pRenderContext = options.pRenderContext; + const ogImageWidth = options.ogImageWidth; + const ogImageHeight = options.ogImageHeight; + const splitPoint = options.splitPoint; + const gapSize = options.gapSize; + const onlyDoGap = options.onlyDoGap; + const involvedGeometryCoords = options.involvedGeometryCoords; + const turnDirection = options.turnDirection; + const turnDirectionConfig = getTurnDirectionConfig(turnDirection); + + const gapCloserPointData = getGapCloserPoints({ + coordOnFirstLine: involvedGeometryCoords.coordOnFirstLine, + intersectCoord: involvedGeometryCoords.intersectCoord, + coordOnSecondLine: involvedGeometryCoords.coordOnSecondLine, + mirrorOffset: ogImageHeight / 2, /* half because we anchor at 0.5 */ + }); + const gapCloserPoints = [gapCloserPointData.forwardPoint, gapCloserPointData.backwardPoint]; + + // 1 - Create gap-closing render points + const gapCloserRenderPoints = []; + for (let i = 0; i < 2; i++) { + const gapCloserPoint = gapCloserPoints[i]; + + // This happens when the angle is so narrow, that the length of the corner is larger than the image width. + // We cannot sensibly cut here, we'd have to add another point. Instead, we cut the second half in the beginning to match the first. + if (!gapCloserPoint.isFirst && gapCloserPoints[0].cutLength > pImageWidth) { + gapCloserPoint.cutInFront = gapCloserPoints[0].cutLength - pImageWidth; + } + + const imageAnchor = gapCloserPoint.isFirst + ? [0, 0.5] + : [1, 0.5]; + const gapCloserImage = await clipToIcon({ + imageStyle: pImage, + clipper: getClippedImageForTurn, + clipInfo: gapCloserPoint, + canvasSize: [ogImageWidth, ogImageHeight], + clipperOptions: { + turnDirection, + }, + anchor: imageAnchor, + }); + + const gapCloserRenderer = toContext(pRenderContext); + + gapCloserRenderPoints.push({ + image: gapCloserImage, + angle: gapCloserPoint.angle, + coords: gapCloserPoint.intersectCoords, + rendererToUse: gapCloserRenderer, + [turnDirectionConfig.firstGapFlag]: gapCloserPoint.isFirst, + [turnDirectionConfig.secondGapFlag]: !gapCloserPoint.isFirst, + isClipped: true, + }); + } + // \1 - finished + + if (onlyDoGap) { + return [ + ...gapCloserRenderPoints, + ]; + } + + // 2 - Now add the actual next segment, past the gap closers + const nextSegmentCutRatio = (splitPoint.segmentLength || gapSize) / gapSize; + const nextSegmentCutLength = nextSegmentCutRatio * ogImageWidth; + const nextSegmentClipInfo = { + cutLength: nextSegmentCutLength, + }; + const nextSegmentImageAnchor = [0, 0.5]; + const nextSegmentImage = await clipToIcon({ + imageStyle: pImage, + clipper: getClippedImageNoAngle, + clipInfo: nextSegmentClipInfo, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: nextSegmentImageAnchor, + }); + + const nextSegmentRenderer = toContext(pRenderContext); + + const nextSegmentRenderPoint = { + // ignore: true, + image: nextSegmentImage, + angle: gapCloserPointData.backwardPoint.angle, + coords: gapCloserPointData.backwardPoint.intersectCoords, + rendererToUse: nextSegmentRenderer, + geometryCoordIndex: currentGeometryCoordIndex, + [turnDirectionConfig.nextSegmentFlag]: true, + isClipped: nextSegmentCutRatio < 1, + clippedAtLength: nextSegmentCutLength, + }; + // \2 - finished + + const result = [ + ...gapCloserRenderPoints, + nextSegmentRenderPoint, + ]; + return result; +} + +/** + * Clip and render a straight segment whose available length is shorter than one + * full symbol width. + * @param {Object} options Shortened-segment rendering options. + * @returns {Promise>} Render data for the shortened point. + */ +async function handleRegularButShortened(options) { + const point = options.point; + const gapSize = options.gapSize; + const ogImageWidth = options.ogImageWidth; + const ogImage = options.ogImage; + let image = options.image; + const renderCoords = options.renderCoords; + const customRender = options.customRender; + const currentGeometryCoordIndex = options.currentGeometryCoordIndex; + + const cutRatio = point.segmentLength / gapSize; + const cutLength = cutRatio * ogImageWidth; + const imageAnchor = point.isFirstOfGeometry + ? [0.5, 0.5] + : [0, 0.5]; + image = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, + clipInfo: { + cutLength: point.isFirstOfGeometry + ? ogImageWidth - cutLength + : cutLength, + cutInFront: false, + cutOnBothEnds: point.isFirstOfGeometry, + }, + canvasSize: [ogImageWidth, options.ogImageHeight], + anchor: imageAnchor, + }); + + const result = [ + { + // ignore: true, + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; + return result; +} + +/** + * Render the legacy half-image left-turn path by clipping the current point and + * retroactively adjusting the incoming segment. + * @param {Object} options Turn-rendering options. + * @returns {Promise>} Render data for the current split point. + */ +async function handleHalfImageLeftTurn(options) { + const { + pixelCoords, + point, + currentGeometryCoordIndex, + ogImageWidth, + ogImageHeight, + ogImage, + renderContext, + pointsDataToRender, + gapSize, + } = options; + + const mirroredCoords = getMirroredCoords( + pixelCoords[currentGeometryCoordIndex - 1], + pixelCoords[currentGeometryCoordIndex], + pixelCoords[currentGeometryCoordIndex + 1], + true, + ogImageHeight / 2, ); + const cutAngle = angleInRadiansAtB( + mirroredCoords.intersect, + pixelCoords[currentGeometryCoordIndex], + pixelCoords[currentGeometryCoordIndex + 1], + ); + const cutLength = point.segmentLength || gapSize; + const clipInfo = { + isFirst: false, + isRightTurn: false, + cutRatio: cutLength / gapSize, + cutHeight: 0.5 * ogImageHeight, + cutAngle, + }; - splitPoints.forEach(point => { - const splitPointAngle = image.getRotation() + point[2]; - render.setImageStyle2(image, splitPointAngle); - render.drawPoint(new Point([point[0] / pixelRatio, point[1] / pixelRatio])); + const image = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageForLeftTurn, + clipInfo, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], }); + + const firstHalfOfLeftTurnRenderData = pointsDataToRender[pointsDataToRender.length - 1]; + if (firstHalfOfLeftTurnRenderData) { + const firstHalfOfLeftTurnCutRatio = firstHalfOfLeftTurnRenderData.clippedAtLength / ogImageWidth; + const adjustingClipInfo = { + isFirst: true, + isRightTurn: false, + cutRatio: firstHalfOfLeftTurnCutRatio, + cutHeight: 0.5 * ogImageHeight, + cutAngle, + }; + firstHalfOfLeftTurnRenderData.image = await clipToIcon({ + imageStyle: firstHalfOfLeftTurnRenderData.image, + clipper: getClippedImageForLeftTurn, + clipInfo: adjustingClipInfo, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: firstHalfOfLeftTurnRenderData.fromSplitPoint.isFirstOfGeometry + ? [0.5, 0.5] + : firstHalfOfLeftTurnRenderData.image.anchor_, + }); + + const hasRenderDataBeforeTurnOnSameSegment = pointsDataToRender.length - 2 >= 0 + && pointsDataToRender[pointsDataToRender.length - 2].geometryCoordIndex === firstHalfOfLeftTurnRenderData.geometryCoordIndex; + if (hasRenderDataBeforeTurnOnSameSegment) { + const lastRenderDataBeforeTurn = pointsDataToRender[pointsDataToRender.length - 2]; + const incomingSpacing = calculatePointsDistance( + lastRenderDataBeforeTurn.coords, + firstHalfOfLeftTurnRenderData.coords, + ); + const firstHalfOfLeftTurnVisibleLength = Number.isFinite(firstHalfOfLeftTurnRenderData.clippedAtLength) + ? Math.min(firstHalfOfLeftTurnRenderData.clippedAtLength, ogImageWidth) + : ogImageWidth; + const uncoveredFrontRatio = 1 - (firstHalfOfLeftTurnVisibleLength / ogImageWidth); + const incomingSpacingRatio = incomingSpacing / gapSize; + const shouldClipPreviousIncomingPoint = incomingSpacingRatio + 1e-11 < uncoveredFrontRatio; + + if (shouldClipPreviousIncomingPoint) { + const lastRenderDataBeforeTurnCutRatio = calculatePointsDistance( + lastRenderDataBeforeTurn.coords, + point.splitPointCoords, + ) / gapSize; + const lastRenderDataBeforeTurnCutLength = lastRenderDataBeforeTurnCutRatio * ogImageWidth; + lastRenderDataBeforeTurn.image = await clipToIcon({ + imageStyle: lastRenderDataBeforeTurn.image, + clipper: getClippedImageNoAngle, + clipInfo: { + cutLength: lastRenderDataBeforeTurnCutLength, + cutInFront: false, + }, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: lastRenderDataBeforeTurn.image.anchor_, + }); + } + } + } + + return [ + { + image, + angle: point.angle, + coords: point.splitPointCoords, + rendererToUse: toContext(renderContext), + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; +} + +/** + * Render the first regular symbol after a half-image left turn, preserving the + * preexisting straight-segment clipping rules. + * @param {Object} options Per-split-point rendering context. + * @returns {Promise>} Render data for the current split point. + */ +async function handleHalfImageFirstAfterLeftTurn(options) { + const { + i, + point, + splitPoints, + gapSize, + ogImageWidth, + ogImageHeight, + ogImage, + image, + renderCoords, + customRender, + currentGeometryCoordIndex, + } = options; + let clippedImage; + + if (point.segmentLength === null || point.segmentLength === undefined) { + const distanceToPreviousSpPointInGapSize = calculatePointsDistance( + point.splitPointCoords, + splitPoints[i - 1].splitPointCoords, + ); + const distanceToPreviousSpPointRatio = distanceToPreviousSpPointInGapSize / gapSize; + const distanceToPreviousSpPoint = distanceToPreviousSpPointRatio * ogImageWidth; + + if (distanceToPreviousSpPoint + 1e-11 < ogImageWidth) { + clippedImage = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, + clipInfo: { + cutLength: distanceToPreviousSpPoint, + cutInFront: true, + cutOnBothEnds: false, + }, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], + }); + } else { + clippedImage = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, + clipInfo: {}, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], + }); + } + } else { + clippedImage = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, + clipInfo: { + cutLength: point.segmentLength, + cutInFront: false, + cutOnBothEnds: true, + }, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], + }); + } + + return [ + { + image: clippedImage || image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; } /** - * Create a renderer function for renderining GraphicStroke marks + * Thin wrapper that routes full-image left turns into the shared turn + * implementation. + * @param {Object} options Turn-rendering options. + * @returns {Promise>} Render data for the turn. + */ +async function handleLeftTurn(options) { + return handleTurn({ + ...options, + turnDirection: 'left', + }); +} + +/** + * Clip an icon into one half of a corner using the shared turn geometry for the + * configured turn direction. + * @param {Object} options Clip inputs and canvas dimensions. + * @returns {Promise} Data URL of the clipped image. + */ +function getClippedImageForTurn(options) { + const img = options.img; + const clipInfo = options.clipInfo; + const canvasWidth = options.canvasWidth; + const canvasHeight = options.canvasHeight; + const turnDirection = options.turnDirection; + const turnDirectionConfig = getTurnDirectionConfig(turnDirection); + + return new Promise((res, _) => { + const canvas = document.createElement('canvas'); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + const ctx = canvas.getContext('2d'); + const cutLength = clipInfo.cutLength; + const canvasDiagonal = Math.sqrt( + (canvasWidth > cutLength + ? canvas.width ** 2 + : cutLength ** 2) + canvas.height ** 2, + ); // This is the max distance within the canvas + + const clipInfoHashCode = getHashCode({ + angle: clipInfo.angle, + cutAngle: clipInfo.cutAngle, + cutLength: clipInfo.cutLength, + canvasDiagonal: clipInfo.canvasDiagonal, + isFirst: clipInfo.isFirst, + img: img.src, + }); + const clipCache = clipInfoHashToBase64[turnDirectionConfig.clipCacheKey]; + if (USE_CACHING && clipCache.has(clipInfoHashCode)) { + return res(clipCache.get(clipInfoHashCode).base64); + } + + ctx.save(); + ctx.beginPath(); + const edgeY = turnDirectionConfig.getEdgeY(canvasHeight); + const oppositeY = turnDirectionConfig.getOppositeY(canvasHeight); + const angledYOffset = turnDirectionConfig.angledYDirection + * Math.sin(clipInfo.cutAngle) * canvasDiagonal; + + if (clipInfo.isFirst) { + ctx.moveTo(0, edgeY); + ctx.lineTo(cutLength, edgeY); + const angledX = cutLength + Math.cos(Math.PI - clipInfo.cutAngle) * canvasDiagonal; + const angledY = edgeY + angledYOffset; + ctx.lineTo(angledX, angledY); + ctx.lineTo(0, oppositeY); + ctx.closePath(); + ctx.clip(); + } else { + ctx.moveTo(canvasWidth, edgeY); + ctx.lineTo(canvasWidth - cutLength, edgeY); + const angledX = canvasWidth - cutLength + Math.cos(clipInfo.cutAngle) * canvasDiagonal; + const angledY = edgeY + angledYOffset; + ctx.lineTo(angledX, angledY); + ctx.lineTo(canvasWidth, oppositeY); + ctx.closePath(); + ctx.clip(); + } + + if (img.complete) { + ctx.drawImage(img, 0, 0); + ctx.restore(); + + const result = canvas.toDataURL(); + if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { + // We don't cache empty canvasses + clipCache.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + res(result); + } else { + img.onload = () => { + ctx.drawImage(img, 0, 0); + ctx.restore(); + + const result = canvas.toDataURL(); + if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { + // We don't cache empty canvasses + clipCache.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + res(result); + }; + } + }); +} + +/** + * Clip an icon into one half of a half-image left turn using the legacy + * left-turn polygon shape. + * @param {Object} options Clip inputs and canvas dimensions. + * @returns {Promise} Data URL of the clipped image. + */ +function getClippedImageForLeftTurn(options) { + const { + img, + clipInfo, + canvasWidth, + canvasHeight, + } = options; + + return new Promise((res, _) => { + const canvas = document.createElement('canvas'); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + const ctx = canvas.getContext('2d'); + const canvasDiagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2); + const cutLength = clipInfo.cutRatio + ? clipInfo.cutRatio * canvas.width + : undefined; + + const clipInfoHashCode = getHashCode({ + cutAngle: clipInfo.cutAngle, + cutLength, + canvasDiagonal, + isFirst: clipInfo.isFirst, + img: img.src, + }); + if (USE_CACHING && clipInfoHashToBase64.leftTurn.has(clipInfoHashCode)) { + return res(clipInfoHashToBase64.leftTurn.get(clipInfoHashCode).base64); + } + + ctx.save(); + ctx.beginPath(); + + if (clipInfo.isFirst) { + const width = cutLength && cutLength < canvas.width + ? cutLength + : canvas.width; + const leftEdge = 0; + const rightEdge = width; + + ctx.moveTo(leftEdge, 0); + ctx.lineTo(leftEdge, clipInfo.cutHeight); + ctx.lineTo(rightEdge, clipInfo.cutHeight); + const invertedCutAngle = Math.PI + clipInfo.cutAngle; + const angledX = rightEdge - Math.cos(invertedCutAngle) * canvasDiagonal; + const angledY = clipInfo.cutHeight + Math.sin(invertedCutAngle) * canvasDiagonal; + ctx.lineTo(angledX, angledY); + ctx.closePath(); + ctx.clip(); + + ctx.beginPath(); + ctx.rect(leftEdge, 0, rightEdge, canvas.height); + ctx.clip(); + } else { + const width = cutLength || canvas.width; + const leftEdge = 0.5 * canvas.width - 0.5 * width; + const rightEdge = 0.5 * canvas.width + 0.5 * width; + + ctx.moveTo(leftEdge, clipInfo.cutHeight); + ctx.lineTo(rightEdge, clipInfo.cutHeight); + ctx.lineTo(rightEdge, 0); + const invertedCutAngle = Math.PI + clipInfo.cutAngle; + const angledX = leftEdge + Math.cos(invertedCutAngle) * canvasDiagonal; + const angledY = clipInfo.cutHeight + Math.sin(invertedCutAngle) * canvasDiagonal; + ctx.lineTo(angledX, angledY); + ctx.closePath(); + ctx.clip(); + + ctx.beginPath(); + ctx.rect(0, 0, rightEdge, canvas.height); + ctx.clip(); + } + + if (img.complete) { + ctx.drawImage(img, 0, 0); + ctx.restore(); + + const result = canvas.toDataURL(); + if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { + clipInfoHashToBase64.leftTurn.set(clipInfoHashCode, { + base64: result, + clipInfo, + }); + } + + res(result); + } else { + img.onload = () => { + ctx.drawImage(img, 0, 0); + ctx.restore(); + + const result = canvas.toDataURL(); + if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { + clipInfoHashToBase64.leftTurn.set(clipInfoHashCode, { + base64: result, + clipInfo, + }); + } + + res(result); + }; + } + }); +} + +/** + * Clip an icon without any angled corner geometry, optionally trimming one or + * both straight ends. + * @param {Object} options Clip inputs and canvas dimensions. + * @returns {Promise} Data URL of the clipped image. + */ +function getClippedImageNoAngle(options) { + const img = options.img; + const clipInfo = options.clipInfo; + const canvasWidth = options.canvasWidth; + const canvasHeight = options.canvasHeight; + + return new Promise((res, _) => { + const canvas = document.createElement('canvas'); + canvas.width = canvasWidth; + canvas.height = canvasHeight; + const ctx = canvas.getContext('2d'); + + if (clipInfo.cutLength === undefined + || clipInfo.cutLength === null + || clipInfo.cutLength >= canvasWidth) { + // No reason to cut, return unchanged + ctx.drawImage(img, 0, 0); + ctx.restore(); + return res(canvas.toDataURL()); + } + + const clipInfoHashCode = getHashCode({ + cutLength: clipInfo.cutLength, + cutInFront: clipInfo.cutInFront, + cutOnBothEnds: clipInfo.cutOnBothEnds, + img: img.src, + }); + if (USE_CACHING && clipInfoHashToBase64.straight.has(clipInfoHashCode)) { + return res(clipInfoHashToBase64.straight.get(clipInfoHashCode).base64); + } + + ctx.beginPath(); + + if (clipInfo.cutInFront) { + ctx.rect(canvasWidth - clipInfo.cutLength, 0, canvasWidth, canvas.height); + } else if (clipInfo.cutOnBothEnds) { + ctx.rect(0.5 * clipInfo.cutLength, 0, canvasWidth - clipInfo.cutLength, canvas.height); + } else { + ctx.rect(0, 0, clipInfo.cutLength, canvas.height); + } + + ctx.clip(); + + if (img.complete) { + ctx.drawImage(img, 0, 0); + ctx.restore(); + + const result = canvas.toDataURL(); + if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { + // We don't cache empty canvasses + clipInfoHashToBase64.straight.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + + res(result); + } else { + img.onload = () => { + ctx.drawImage(img, 0, 0); + ctx.restore(); + + const result = canvas.toDataURL(); + if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { + // We don't cache empty canvasses + clipInfoHashToBase64.straight.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + + res(result); + }; + } + }); +} + +/** + * Rendering the point, including a fallback if img is not loaded yet, retrying in 10ms. + * @param options + */ +function renderPoint(options) { + const { + image, + angle, + coords, + renderToUse, + pixelRatio, + renderSession, + feature, + } = options; + + // If this render session is not current, don't render anything here. + if (currentRenderSessionByFeatureOlUid.get(feature.ol_uid) !== renderSession) { + return; + } + + const imgElement = image.getImage(pixelRatio); + + // Check if image is ready + if (!imgElement || !imgElement.complete || imgElement.naturalWidth === 0) { + // Recursively call this function once the image is read + imgElement.onload = () => renderPoint({ + image, + angle, + coords, + renderToUse, + pixelRatio, + renderSession, + feature, + }); + return; + } + + const imageAngle = image.getRotation() + angle; + patchRenderer(renderToUse); + renderToUse.setImageStyle2(image, imageAngle); + const pointToDraw = new Point([ + coords[0] / pixelRatio, + coords[1] / pixelRatio, + ]); + + renderToUse.drawPoint(pointToDraw); +} + +/** + * Build a deterministic numeric hash for cache keys derived from shallow + * objects of primitive values. + * @param {Object} object Source object for the hash. + * @returns {number} Numeric hash code. + */ +function getHashCode(object) { + if (!USE_CACHING) { + return 1; + } + + const keys = Object.keys(object) + .sort(); + let hash = 0; + + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + const value = object[key]; + + // Hash the key + for (let j = 0; j < key.length; j++) { + hash = ((hash << 5) - hash) + key.charCodeAt(j); + hash = hash & hash; + } + + // Hash the value based on its type + if (typeof value === 'number') { + // Convert number to string with high precision to ensure uniqueness + // This prevents hash collisions for slightly different float values + // const numberString = value.toPrecision(15); + const numberString = value.toPrecision(11); + for (let j = 0; j < numberString.length; j++) { + hash = ((hash << 5) - hash) + numberString.charCodeAt(j); + hash = hash & hash; + } + } else if (typeof value === 'boolean') { + hash = ((hash << 5) - hash) + (value ? 1 : 0); + } else if (typeof value === 'string') { + for (let j = 0; j < value.length; j++) { + hash = ((hash << 5) - hash) + value.charCodeAt(j); + hash = hash & hash; + } + } + hash = hash & hash; + } + + return hash; +} + +/** + * Compare a canvas data URL against the cached empty-canvas output for the same + * dimensions. + * @param {string} canvasSrc Canvas data URL to test. + * @param {number} canvasWidth Canvas width in pixels. + * @param {number} canvasHeight Canvas height in pixels. + * @returns {boolean} `true` when the canvas is empty. + */ +function isCanvasEmpty(canvasSrc, canvasWidth, canvasHeight) { + // if (!emptyCanvasSrc) { + // const emptyCanvas = document.createElement('canvas'); + // emptyCanvas.width = canvasWidth; + // emptyCanvas.height = canvasHeight; + // emptyCanvasSrc = emptyCanvas.toDataURL(); + // } + // + // return canvasSrc === emptyCanvasSrc; + + // Create a unique key for this canvas size + const sizeKey = `${canvasWidth}x${canvasHeight}`; + + // Use a Map to cache empty canvas data URLs by size + if (!window._emptyCanvasCache) { + window._emptyCanvasCache = new Map(); + } + + if (!window._emptyCanvasCache.has(sizeKey)) { + const emptyCanvas = document.createElement('canvas'); + emptyCanvas.width = canvasWidth; + emptyCanvas.height = canvasHeight; + window._emptyCanvasCache.set(sizeKey, emptyCanvas.toDataURL()); + } + + return canvasSrc === window._emptyCanvasCache.get(sizeKey); + +} + +/** + * Create a DOM image element for a source URL so it can be used by the canvas + * clipping helpers. + * @param {string} src Image source URL or data URL. + * @returns {HTMLImageElement} Image element with the source assigned. + */ +function createDomImage(src) { + const img = new Image(); + img.src = src; + return img; +} + +/** + * Run a clipping helper and wrap the resulting data URL back into an OpenLayers + * icon with the requested anchor. + * @param {Object} options Clip configuration and source icon. + * @returns {Promise} Clipped OpenLayers icon. + */ +async function clipToIcon(options) { + const { + imageStyle, + clipper, + clipInfo, + canvasSize, + anchor, + clipperOptions = {}, + } = options; + + const imgSize = getImageStyleSize(imageStyle); + const scale = imageStyle.getScale(); + const [canvasWidth, canvasHeight] = canvasSize || imgSize; + const img = createDomImage(getImageStyleSrc(imageStyle)); + const clippedSrc = await clipper({ + img, + clipInfo, + canvasWidth, + canvasHeight, + ...clipperOptions, + }); + + return createOlIconWithDataURL({ + src: clippedSrc, + imgSize, + scale, + anchor, + }); +} + +/** + * Resolve the source URL used by an OpenLayers icon style. + * @param {Icon} imageStyle OpenLayers icon style. + * @returns {string|undefined} Image source URL. + */ +function getImageStyleSrc(imageStyle) { + return imageStyle.iconImage_?.src_ || imageStyle.getSrc?.(); +} + +/** + * Resolve the pixel size used by an OpenLayers icon style across OL versions. + * @param {Icon} imageStyle OpenLayers icon style. + * @returns {Array|undefined} Image size as `[width, height]`. + */ +function getImageStyleSize(imageStyle) { + return imageStyle.getSize?.() + || imageStyle.imgSize_ + || imageStyle.size_ + || imageStyle.iconImage_?.size_; +} + +/** + * Used to handle image src loading, because leaving that up to OL sometimes causes the images to not render. + * @param {Object} options Configuration options for creating the icon + * @param {string} options.src The data URL source of the icon image + * @param {Array} options.imgSize The size of the image in pixels [width, height] + * @param {number} options.scale The scale factor to apply to the image + * @param {Array} options.anchor The anchor point of the icon as fraction [x, y] + * @returns {Icon} An OpenLayers Icon instance with the specified image and settings + */ +function createOlIconWithDataURL(options) { + const { + src, + imgSize, + scale, + anchor, + } = options; + + const tempImg = new Image(); + tempImg.src = src; + if (imgSize && imgSize[0] && imgSize[1]) { + tempImg.width = imgSize[0]; + tempImg.height = imgSize[1]; + } + + const icon = new Icon({ + img: tempImg, + imgSize: imgSize, + size: imgSize, // OL10 needs size when img is provided + scale: scale, + anchor: anchor, + anchorXUnits: 'fraction', + anchorYUnits: 'fraction', + }); + + return icon; +} + +/** + * Create a renderer function for rendering GraphicStroke marks * to be used inside an OpenLayers Style.renderer function. * @private * @param {LineSymbolizer} linesymbolizer SLD line symbolizer object. @@ -121,7 +1567,7 @@ function renderStrokeMarks( export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { if (!(linesymbolizer.stroke && linesymbolizer.stroke.graphicstroke)) { throw new Error( - 'getGraphicStrokeRenderer error: symbolizer.stroke.graphicstroke null or undefined.' + 'getGraphicStrokeRenderer error: symbolizer.stroke.graphicstroke null or undefined.', ); } @@ -142,7 +1588,8 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { return (pixelCoords, renderState) => { // Abort when feature geometry is (Multi)Point. - const geometryType = renderState.feature.getGeometry().getType(); + const geometryType = renderState.feature.getGeometry() + .getType(); if (geometryType === 'Point' || geometryType === 'MultiPoint') { return; } @@ -150,8 +1597,7 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { const pixelRatio = renderState.pixelRatio || 1.0; // TODO: Error handling, alternatives, etc. - const render = toContext(renderState.context); - patchRenderer(render); + const renderContext = renderState.context; let defaultGraphicSize = DEFAULT_MARK_SIZE; if (graphicstroke.graphic && graphicstroke.graphic.externalgraphic) { @@ -161,7 +1607,7 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { const pointStyle = getPointStyle( graphicstroke, renderState.feature, - getProperty + getProperty, ); // Calculate graphic spacing. @@ -175,20 +1621,22 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { graphicSizeExpression, renderState.feature, getProperty, - defaultGraphicSize - ) + defaultGraphicSize, + ), ); const graphicSpacing = calculateGraphicSpacing(linesymbolizer, graphicSize); options.initialGap = getInitialGapSize(linesymbolizer); renderStrokeMarks( - render, + renderContext, pixelCoords, graphicSpacing, pointStyle, pixelRatio, - options + options, + geometryType, + renderState.feature, ); }; } @@ -203,7 +1651,7 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { function getGraphicStrokeStyle(linesymbolizer, getProperty) { if (!(linesymbolizer.stroke && linesymbolizer.stroke.graphicstroke)) { throw new Error( - 'getGraphicStrokeStyle error: linesymbolizer.stroke.graphicstroke null or undefined.' + 'getGraphicStrokeStyle error: linesymbolizer.stroke.graphicstroke null or undefined.', ); }