From 0a628e8fdd536531f1521e9b291fa51e1292efd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Wed, 31 Jul 2024 19:39:46 +0200 Subject: [PATCH 01/27] prepared fork. --- docs/_config.yml | 2 +- package-lock.json | 8 ++++---- package.json | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) 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/package-lock.json b/package-lock.json index 9f98ebee..a1c7b6e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "@nieuwlandgeo/sldreader", - "version": "0.4.3", + "name": "@disy/sldreader", + "version": "0.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@nieuwlandgeo/sldreader", - "version": "0.4.3", + "name": "@disy/sldreader", + "version": "0.5.0", "license": "ISC", "devDependencies": { "@rollup/plugin-buble": "^1.0.2", diff --git a/package.json b/package.json index 60bd6f06..907969f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "@nieuwlandgeo/sldreader", - "version": "0.4.3", + "name": "@disy/sldreader", + "version": "0.5.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": [ From aecf20d65ca95717b432f149e161006d1be7b397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Wed, 31 Jul 2024 19:40:40 +0200 Subject: [PATCH 02/27] adjusted rendering of ExternalGraphic in GraphicStroke, supporting line ends. --- docs/assets/sldreader.js | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/assets/sldreader.js b/docs/assets/sldreader.js index 6e0cf095..942b8b13 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,30 @@ 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. + + 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 +2736,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. From 1f0e2efe63b9e82db0a1b1e645e5e4e266a65c5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Wed, 31 Jul 2024 19:51:06 +0200 Subject: [PATCH 03/27] adjusted rendering of ExternalGraphic in GraphicStroke, supporting line ends. --- src/styles/geometryCalcs.js | 65 ++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index 15d808a5..4a4824aa 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -33,8 +33,10 @@ function calculateAngle(p1, p2, invertY) { } // eslint-disable-next-line import/prefer-default-export -export function splitLineString(geometry, graphicSpacing, options = {}) { - const coords = geometry.getCoordinates(); +export function splitLineString(geometry, graphicSpacing, options) { + if ( options === void 0 ) options = {}; + + var coords = geometry.getCoordinates(); // Handle degenerate cases. // LineString without points @@ -44,46 +46,62 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { // 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]; + var p1 = coords[0]; + var 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]; - const p2 = coords[coords.length - 1]; - return [[p2[0], p2[1], calculateAngle(p1, p2, options.invertY)]]; + var p1$1 = coords[coords.length - 2]; + var p2$1 = coords[coords.length - 1]; + return [[p2$1[0], p2$1[1], calculateAngle(p1$1, p2$1, options.invertY)]]; } - const totalLength = geometry.getLength(); - const gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. + 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. - let nextPointMeasure = options.initialGap || 0.0; - let pointIndex = 0; - const currentSegmentStart = [...coords[0]]; - const currentSegmentEnd = [...coords[1]]; + var nextPointMeasure = gapSize / 2; + var pointIndex = 0; + var currentSegmentStart = [].concat( coords[0] ); + var currentSegmentEnd = [].concat( coords[1] ); // Cumulative measure of the line where each segment's length is added in succession. - let cumulativeMeasure = 0; + var cumulativeMeasure = 0; - const splitPoints = []; + var splitPoints = []; // Keep adding points until the next point measure lies beyond the line length. - while (nextPointMeasure <= totalLength) { - const currentSegmentLength = calculatePointsDistance( + 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. + + 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; @@ -93,24 +111,25 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { 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. - const distanceFromSegmentStart = nextPointMeasure - cumulativeMeasure; - const splitPointCoords = calculateSplitPointCoords( + var distanceFromSegmentStart = nextPointMeasure - cumulativeMeasure; + var splitPointCoords = calculateSplitPointCoords( currentSegmentStart, currentSegmentEnd, distanceFromSegmentStart ); - const angle = calculateAngle( + var angle = calculateAngle( currentSegmentStart, currentSegmentEnd, options.invertY ); if ( !options.extent || - containsCoordinate(options.extent, splitPointCoords) + extent.containsCoordinate(options.extent, splitPointCoords) ) { splitPointCoords.push(angle); splitPoints.push(splitPointCoords); From ea3273200374aba95aa0e58d1dd4dd4fe7a154c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Wed, 31 Jul 2024 23:18:12 +0200 Subject: [PATCH 04/27] adjusted rendering of ExternalGraphic in GraphicStroke, supporting line ends. --- package.json | 2 +- src/styles/geometryCalcs.js | 50 ++++++++++++++++++------------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 907969f6..51e9de59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.5.0", + "version": "0.5.1", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index 4a4824aa..e0d7a9c3 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -33,10 +33,8 @@ function calculateAngle(p1, p2, invertY) { } // eslint-disable-next-line import/prefer-default-export -export function splitLineString(geometry, graphicSpacing, options) { - if ( options === void 0 ) options = {}; - - var coords = geometry.getCoordinates(); +export function splitLineString(geometry, graphicSpacing, options = {}) { + const coords = geometry.getCoordinates(); // Handle degenerate cases. // LineString without points @@ -46,42 +44,42 @@ export function splitLineString(geometry, graphicSpacing, options) { // LineString containing only one point. if (coords.length === 1) { - return [( coords[0] ).concat( [0])]; + return [(coords[0]).concat([0])]; } // Handle first point placement case. if (options.placement === PLACEMENT_FIRSTPOINT) { - var p1 = coords[0]; - var p2 = coords[1]; + 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) { - var p1$1 = coords[coords.length - 2]; - var p2$1 = coords[coords.length - 1]; + const p1$1 = coords[coords.length - 2]; + const p2$1 = coords[coords.length - 1]; return [[p2$1[0], p2$1[1], calculateAngle(p1$1, p2$1, options.invertY)]]; } - var gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. + 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. - var nextPointMeasure = gapSize / 2; - var pointIndex = 0; - var currentSegmentStart = [].concat( coords[0] ); - var currentSegmentEnd = [].concat( coords[1] ); + let nextPointMeasure = gapSize / 2; + let pointIndex = 0; + const currentSegmentStart = [].concat(coords[0]); + const currentSegmentEnd = [].concat(coords[1]); // Cumulative measure of the line where each segment's length is added in succession. - var cumulativeMeasure = 0; + let cumulativeMeasure = 0; - var splitPoints = []; + const splitPoints = []; // Keep adding points until the next point measure lies beyond the line length. while (true) { - var currentSegmentLength = calculatePointsDistance( + 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. @@ -97,7 +95,7 @@ export function splitLineString(geometry, graphicSpacing, options) { options.invertY ); if (!options.extent - || extent.containsCoordinate(options.extent, splitPointCoords)) { + || containsCoordinate(options.extent, splitPointCoords)) { splitPointCoords.push(angle); splitPoints.push(splitPointCoords); } @@ -112,24 +110,24 @@ export function splitLineString(geometry, graphicSpacing, options) { currentSegmentEnd[1] = coords[pointIndex + 2][1]; pointIndex += 1; cumulativeMeasure = 0; - nextPointMeasure = gapSize / 2 + nextPointMeasure = gapSize / 2; } else { // Next point lies on the current segment. // Calculate its position and increase next point measure by gap size. - var distanceFromSegmentStart = nextPointMeasure - cumulativeMeasure; - var splitPointCoords = calculateSplitPointCoords( + const distanceFromSegmentStart = nextPointMeasure - cumulativeMeasure; + const splitPointCoords = calculateSplitPointCoords( currentSegmentStart, currentSegmentEnd, - distanceFromSegmentStart + distanceFromSegmentStart, ); - var angle = calculateAngle( + const angle = calculateAngle( currentSegmentStart, currentSegmentEnd, - options.invertY + options.invertY, ); if ( !options.extent || - extent.containsCoordinate(options.extent, splitPointCoords) + containsCoordinate(options.extent, splitPointCoords) ) { splitPointCoords.push(angle); splitPoints.push(splitPointCoords); From 39f3275c1845904d54b59020c69087d8bfbe30d6 Mon Sep 17 00:00:00 2001 From: swalter-letsdev <82449043+swalter-letsdev@users.noreply.github.com> Date: Wed, 11 Sep 2024 09:59:18 +0200 Subject: [PATCH 05/27] fix images exceeding line length --- src/styles/geometryCalcs.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index e0d7a9c3..0fae05c2 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -81,8 +81,9 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { 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, From 08698d8bfbf56078c8625b95280a414399383f39 Mon Sep 17 00:00:00 2001 From: swalter-letsdev <82449043+swalter-letsdev@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:00:11 +0200 Subject: [PATCH 06/27] fix images exceeding line length --- docs/assets/sldreader.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/assets/sldreader.js b/docs/assets/sldreader.js index 942b8b13..555805c0 100644 --- a/docs/assets/sldreader.js +++ b/docs/assets/sldreader.js @@ -2708,8 +2708,9 @@ 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, From b6848fabcedf1edf3e2771412af0a484fab0651b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Wed, 11 Sep 2024 10:13:49 +0200 Subject: [PATCH 07/27] v0.5.2. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 51e9de59..9f3f769a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.5.1", + "version": "0.5.2", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ From ff57be087bae43008387c449f1e53b8da5d51ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Wed, 11 Sep 2024 10:41:53 +0200 Subject: [PATCH 08/27] v0.5.3. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a1c7b6e6..eda04643 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@disy/sldreader", - "version": "0.5.0", + "version": "0.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@disy/sldreader", - "version": "0.5.0", + "version": "0.5.2", "license": "ISC", "devDependencies": { "@rollup/plugin-buble": "^1.0.2", diff --git a/package.json b/package.json index 9f3f769a..7f8943c7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.5.2", + "version": "0.5.3", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ From 9e1a263eb7b887d88e035d3db885435362c81631 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Wed, 22 Jan 2025 14:54:18 +0100 Subject: [PATCH 09/27] LETSDEV-605 - Fix images overlapping geometry lines --- src/styles/geometryCalcs.js | 145 ++++++++++++++++++++----------- src/styles/graphicStrokeStyle.js | 101 +++++++++++++++++---- 2 files changed, 181 insertions(+), 65 deletions(-) diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index 0fae05c2..198df6bc 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -2,17 +2,33 @@ import { containsCoordinate } from 'ol/extent'; import { PLACEMENT_FIRSTPOINT, PLACEMENT_LASTPOINT } from '../constants'; +/** + * Euclidean distance between two points ([x, y]). + */ 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; +/** + * 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; + + var 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; + } + var x = startCoord[0] + (endCoord[0] - startCoord[0]) * d; + var y = startCoord[1] + (endCoord[1] - startCoord[1]) * d; return [x, y]; } @@ -33,6 +49,10 @@ function calculateAngle(p1, p2, invertY) { } // eslint-disable-next-line import/prefer-default-export +/** + * 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. + */ export function splitLineString(geometry, graphicSpacing, options = {}) { const coords = geometry.getCoordinates(); @@ -49,55 +69,72 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { // Handle first point placement case. if (options.placement === PLACEMENT_FIRSTPOINT) { - const p1 = coords[0]; - const p2 = coords[1]; + var p1 = coords[0]; + var 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$1 = coords[coords.length - 2]; - const p2$1 = coords[coords.length - 1]; + var p1$1 = coords[coords.length - 2]; + var p2$1 = coords[coords.length - 1]; return [[p2$1[0], p2$1[1], calculateAngle(p1$1, p2$1, options.invertY)]]; } - const gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. + 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. - let nextPointMeasure = gapSize / 2; - let pointIndex = 0; - const currentSegmentStart = [].concat(coords[0]); - const currentSegmentEnd = [].concat(coords[1]); + var pointIndex = 0; + var currentSegmentStart = [].concat(coords[0]); + var currentSegmentEnd = [].concat(coords[1]); - // Cumulative measure of the line where each segment's length is added in succession. - let cumulativeMeasure = 0; + var splitPoints = []; - const splitPoints = []; + let splitPointsOnThisSegment = 0; // Keep adding points until the next point measure lies beyond the line length. while (true) { - const currentSegmentLength = calculatePointsDistance( + var currentSegmentLength = calculatePointsDistance( currentSegmentStart, - currentSegmentEnd, + currentSegmentEnd ); - // 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( + 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); + } + + var splitPointCoords = calculateSplitPointCoords({ + startCoord: currentSegmentStart, + endCoord: currentSegmentEnd, + distanceFromStart: distanceFromStart, + graphicWidth: gapSize + }); + var angle = calculateAngle( currentSegmentStart, currentSegmentEnd, options.invertY ); + // Only return split points that will be rendered (are in extent). if (!options.extent - || containsCoordinate(options.extent, splitPointCoords)) { + || extent.containsCoordinate(options.extent, splitPointCoords)) { splitPointCoords.push(angle); + /* + * 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) { + splitPointCoords.push(currentSegmentLength); + } splitPoints.push(splitPointCoords); } @@ -110,32 +147,42 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { currentSegmentEnd[0] = coords[pointIndex + 2][0]; currentSegmentEnd[1] = coords[pointIndex + 2][1]; pointIndex += 1; - cumulativeMeasure = 0; - nextPointMeasure = gapSize / 2; - } 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, - ); - const angle = calculateAngle( + 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 computet 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. + var splitPointCoords$1 = calculateSplitPointCoords({ + startCoord: currentSegmentStart, + endCoord: currentSegmentEnd, + distanceFromStart: distanceFromStart + }); + var angle$1 = 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) + extent.containsCoordinate(options.extent, splitPointCoords$1) ) { - splitPointCoords.push(angle); - splitPoints.push(splitPointCoords); + splitPointCoords$1.push(angle$1); + // 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$1); } - nextPointMeasure += gapSize; + splitPointsOnThisSegment++; } } return splitPoints; -} +} \ No newline at end of file diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 7e80100e..ed548b35 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -53,7 +53,7 @@ function patchRenderer(renderer) { * @returns {void} */ function renderStrokeMarks( - render, + renderContext, pixelCoords, graphicSpacing, pointStyle, @@ -64,13 +64,17 @@ function renderStrokeMarks( return; } + // We use the context as param and create the render object here, because we need to create deep copies later. + const render2 = render.toContext(renderContext); + patchRenderer(render2); + // 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, @@ -87,26 +91,89 @@ 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; } - const splitPoints = splitLineString( - new LineString(pixelCoords), - graphicSpacing * pixelRatio, + const gapSize = graphicSpacing * pixelRatio; + + var splitPoints = splitLineString( + new geom.LineString(pixelCoords), + gapSize, { invertY: true, // Pixel y-coordinates increase downwards in screen space. - extent: render.extent_, + extent: render2.extent_, placement: options.placement, initialGap: options.initialGap, + graphicWidth: ogImage.iconImage_?.image_?.naturalWidth } ); - splitPoints.forEach(point => { - const splitPointAngle = image.getRotation() + point[2]; - render.setImageStyle2(image, splitPointAngle); - render.drawPoint(new Point([point[0] / pixelRatio, point[1] / pixelRatio])); + // Not quite a deep clone, but deep cloning the properties we need for rendering. + // We do this to not affect all individually rendered images when adjusting some of them. + const deepCloneImage = (image) => { + const copy = image.clone(); + + copy.imgSize_ = structuredClone(image.imgSize_); + copy.iconImage_ = new image.iconImage_.__proto__.constructor( + image.iconImage_.image_, + image.iconImage_.src_, + [image.iconImage_.size_[0], image.iconImage_.size_[1]], + image.iconImage_.crossOrigin_, + image.iconImage_.imageState_, + image.iconImage_.color_ + ); + + return copy; + } + + let ogImageWidth; + if (ogImage.imgSize_) { + ogImageWidth = ogImage.imgSize_[0]; + } + + // This loop renders the individual splitPoints. + splitPoints.forEach((point) => { + let customRender = render2; + let image; + + /* This whole function has some adjustment solely for the case of the graphic being wider than a segment of the geometry. + * Whenever this case occurs, we change the width of the image that will be rendered for the respective split point. + * The condition for this case is as follows: `gapSize > segmentLength`, where gapSize is the graphic width in the pixel + * space that's used for the splitLineString computation (which produces the splitPoints array), and segmentLength is the + * length of the segment that might be too short for the graphic (same pixel space as `gapSize`). + * To adjust the image size, we need to apply the ratio of `segmentLength / gapSize` (short segment length / long graphic width) + * to the actual graphic size, since the graphic width is given in a different pixel space. The resulting value will be the + * length of the segment in the pixelspace of the graphic, making the graphic exactly as long as the segment. + * (i.e. if `segmentLength` is 2/3 of `gapSize`, we want the image to be rendered 2/3 as wide as the original) + * For the rendering to work correctly, we need to use a separate render object. Otherwise the image size will be applied to + * every rendered image, due to the static way the render object holds the size information. We create a deep copy of the + * render object for this purpose (only the properties that we require are properly deep copied). + */ + if (ogImage.iconImage_) { + image = deepCloneImage(ogImage); + + const hasSegmentLength = point.length > 3; + if (hasSegmentLength) { + const segmentLength = point[3]; + if (gapSize > segmentLength) { + const imageToSegmentRatio = (segmentLength / gapSize); + const newVal = ogImageWidth * imageToSegmentRatio; + image.iconImage_.size_[0] = newVal; + + customRender = render.toContext(renderContext); + patchRenderer(customRender); + } + } + } else { + image = ogImage; + } + + var splitPointAngle = image.getRotation() + point[2]; + customRender.setImageStyle2(image, splitPointAngle); + const pointToDraw = new geom.Point([point[0] / pixelRatio, point[1] / pixelRatio]); + customRender.drawPoint(pointToDraw); }); } @@ -150,8 +217,9 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { const pixelRatio = renderState.pixelRatio || 1.0; // TODO: Error handling, alternatives, etc. - const render = toContext(renderState.context); - patchRenderer(render); + // const render = toContext(renderState.context); + // patchRenderer(render); + const renderContext = renderState.context; let defaultGraphicSize = DEFAULT_MARK_SIZE; if (graphicstroke.graphic && graphicstroke.graphic.externalgraphic) { @@ -183,7 +251,8 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { options.initialGap = getInitialGapSize(linesymbolizer); renderStrokeMarks( - render, + // render, + renderContext, pixelCoords, graphicSpacing, pointStyle, From 363d8e45ccf65074362e41c5a129d12cc5d09831 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Wed, 22 Jan 2025 16:40:43 +0100 Subject: [PATCH 10/27] LETSDEV-605 - syntax and import fixes --- src/styles/geometryCalcs.js | 4 ++-- src/styles/graphicStrokeStyle.js | 29 +++++++++++++---------------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index 198df6bc..8073279e 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -125,7 +125,7 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { ); // Only return split points that will be rendered (are in extent). if (!options.extent - || extent.containsCoordinate(options.extent, splitPointCoords)) { + || containsCoordinate(options.extent, splitPointCoords)) { splitPointCoords.push(angle); /* * If this is the only split point on this segment, we also add the current segment length. This might be used to @@ -174,7 +174,7 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { // Only return split points that will be rendered (are in extent). if ( !options.extent || - extent.containsCoordinate(options.extent, splitPointCoords$1) + containsCoordinate(options.extent, splitPointCoords$1) ) { splitPointCoords$1.push(angle$1); // 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. diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index ed548b35..d167de38 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -65,8 +65,8 @@ function renderStrokeMarks( } // We use the context as param and create the render object here, because we need to create deep copies later. - const render2 = render.toContext(renderContext); - patchRenderer(render2); + const render = toContext(renderContext); + patchRenderer(render); // 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. @@ -96,17 +96,22 @@ function renderStrokeMarks( return; } + let ogImageWidth = null; + if (ogImage.imgSize_) { + ogImageWidth = ogImage.imgSize_[0]; + } + const gapSize = graphicSpacing * pixelRatio; var splitPoints = splitLineString( - new geom.LineString(pixelCoords), + new LineString(pixelCoords), gapSize, { invertY: true, // Pixel y-coordinates increase downwards in screen space. - extent: render2.extent_, + extent: render.extent_, placement: options.placement, initialGap: options.initialGap, - graphicWidth: ogImage.iconImage_?.image_?.naturalWidth + graphicWidth: ogImageWidth } ); @@ -128,14 +133,9 @@ function renderStrokeMarks( return copy; } - let ogImageWidth; - if (ogImage.imgSize_) { - ogImageWidth = ogImage.imgSize_[0]; - } - // This loop renders the individual splitPoints. splitPoints.forEach((point) => { - let customRender = render2; + let customRender = render; let image; /* This whole function has some adjustment solely for the case of the graphic being wider than a segment of the geometry. @@ -162,7 +162,7 @@ function renderStrokeMarks( const newVal = ogImageWidth * imageToSegmentRatio; image.iconImage_.size_[0] = newVal; - customRender = render.toContext(renderContext); + customRender = toContext(renderContext); patchRenderer(customRender); } } @@ -172,7 +172,7 @@ function renderStrokeMarks( var splitPointAngle = image.getRotation() + point[2]; customRender.setImageStyle2(image, splitPointAngle); - const pointToDraw = new geom.Point([point[0] / pixelRatio, point[1] / pixelRatio]); + const pointToDraw = new Point([point[0] / pixelRatio, point[1] / pixelRatio]); customRender.drawPoint(pointToDraw); }); } @@ -217,8 +217,6 @@ 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; @@ -251,7 +249,6 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { options.initialGap = getInitialGapSize(linesymbolizer); renderStrokeMarks( - // render, renderContext, pixelCoords, graphicSpacing, From 36fc962e91e7bbf5d7d10b322508f8a3b9bb2f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Fri, 7 Feb 2025 00:20:12 +0100 Subject: [PATCH 11/27] v0.5.4. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eda04643..b2882165 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@disy/sldreader", - "version": "0.5.2", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@disy/sldreader", - "version": "0.5.2", + "version": "0.5.4", "license": "ISC", "devDependencies": { "@rollup/plugin-buble": "^1.0.2", diff --git a/package.json b/package.json index 7f8943c7..4d577052 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.5.3", + "version": "0.5.4", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ From f41ef854ed7335c8587f8c6f7f0d59a3e68a3228 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Fri, 26 Sep 2025 16:42:28 +0200 Subject: [PATCH 12/27] added feature signature gap filling algo. --- package-lock.json | 2087 +++++++++++++++++++++++++++++- package.json | 3 + rollup.config.mjs | 44 +- src/styles/geometryCalcs.js | 401 ++++-- src/styles/graphicStrokeStyle.js | 1260 +++++++++++++++++- 5 files changed, 3613 insertions(+), 182 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2882165..d416c511 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.5.4", "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", @@ -36,16 +39,1734 @@ "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.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "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, - "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-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, + "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-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 4d577052..86b0ffb8 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/rollup.config.mjs b/rollup.config.mjs index 52b29b87..7429a56b 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,11 @@ export default { 'ol/has': 'ol.has', }, }, - plugins: [buble({ objectAssign: true }), nodeResolve()], + plugins: [ + babel({ + babelHelpers: 'bundled', + presets: ['@babel/preset-env'] + }), + nodeResolve() + ], }; diff --git a/src/styles/geometryCalcs.js b/src/styles/geometryCalcs.js index 8073279e..ac9108a1 100644 --- a/src/styles/geometryCalcs.js +++ b/src/styles/geometryCalcs.js @@ -2,56 +2,10 @@ import { containsCoordinate } from 'ol/extent'; import { PLACEMENT_FIRSTPOINT, PLACEMENT_LASTPOINT } from '../constants'; -/** - * Euclidean distance between two points ([x, y]). - */ -function calculatePointsDistance(coord1, coord2) { - const dx = coord1[0] - coord2[0]; - const dy = coord1[1] - coord2[1]; - return Math.sqrt(dx * dx + dy * dy); -} - -/** - * 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; - - var 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; - } - var x = startCoord[0] + (endCoord[0] - startCoord[0]) * d; - var y = startCoord[1] + (endCoord[1] - startCoord[1]) * d; - return [x, y]; -} - -/** - * 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; -} - // eslint-disable-next-line import/prefer-default-export /** - * 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. + * 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. */ export function splitLineString(geometry, graphicSpacing, options = {}) { const coords = geometry.getCoordinates(); @@ -61,46 +15,42 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { if (coords.length === 0) { return []; } - // LineString containing only one point. if (coords.length === 1) { return [(coords[0]).concat([0])]; } - // Handle first point placement case. if (options.placement === PLACEMENT_FIRSTPOINT) { - var p1 = coords[0]; - var p2 = coords[1]; + 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) { - var p1$1 = coords[coords.length - 2]; - var p2$1 = coords[coords.length - 1]; - return [[p2$1[0], p2$1[1], calculateAngle(p1$1, p2$1, options.invertY)]]; + const p1 = coords[coords.length - 2]; + const p2 = coords[coords.length - 1]; + return [[p2[0], p2[1], calculateAngle(p1, p2, options.invertY)]]; } - var gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. - - var pointIndex = 0; - var currentSegmentStart = [].concat(coords[0]); - var currentSegmentEnd = [].concat(coords[1]); + const gapSize = Math.max(graphicSpacing, 0.1); // 0.1 px minimum gap size to prevent accidents. - var splitPoints = []; + let pointIndex = 0; + const currentSegmentStart = [].concat(coords[0]); + const currentSegmentEnd = [].concat(coords[1]); let splitPointsOnThisSegment = 0; + const splitPoints = []; // Keep adding points until the next point measure lies beyond the line length. while (true) { - var currentSegmentLength = calculatePointsDistance( + const currentSegmentLength = calculatePointsDistance( currentSegmentStart, - currentSegmentEnd + currentSegmentEnd, ); 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. + // 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) { @@ -112,30 +62,35 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { distanceFromStart = currentSegmentLength - (0.5 * gapSize); } - var splitPointCoords = calculateSplitPointCoords({ + const splitPointCoords = calculateSplitPointCoords({ startCoord: currentSegmentStart, endCoord: currentSegmentEnd, distanceFromStart: distanceFromStart, - graphicWidth: gapSize + graphicWidth: gapSize, }); - var angle = calculateAngle( + 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); + 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 + * 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) { - splitPointCoords.push(currentSegmentLength); + splitPoint.segmentLength = currentSegmentLength; + splitPoint.isOnlySpOnThisSegment = true; } - splitPoints.push(splitPointCoords); + splitPoints.push(splitPoint); } if (pointIndex === coords.length - 2) { @@ -154,35 +109,311 @@ export function splitLineString(geometry, graphicSpacing, options = {}) { // 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 computet thus far, we include the half gapsize of the first point, hence `+ 0.5`. + // 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. - var splitPointCoords$1 = calculateSplitPointCoords({ + const splitPointCoords = calculateSplitPointCoords({ startCoord: currentSegmentStart, endCoord: currentSegmentEnd, - distanceFromStart: distanceFromStart + distanceFromStart: distanceFromStart, }); - var angle$1 = calculateAngle( + 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$1) + containsCoordinate(options.extent, splitPointCoords) ) { - splitPointCoords$1.push(angle$1); - // 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$1); + // 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, + }); } splitPointsOnThisSegment++; } } return splitPoints; -} \ No newline at end of file +} + +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 d167de38..27e96e4f 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,50 @@ import { import evaluate from '../olEvaluator'; import getPointStyle from './pointStyle'; import { calculateGraphicSpacing, getInitialGapSize } from './styleUtils'; -import { splitLineString } from './geometryCalcs'; +import { + splitLineString, + getGapCloserPoints, + calculatePointsDistance, + angleInRadiansAtB, + getMirroredCoords, + getIsRightTurn, +} from './geometryCalcs'; + +// Performance tracking +let perfMetrics = { + renderStrokeMarks: 0, + handleRightTurn: 0, + handleLeftTurn: 0, + getClippedImageForRightTurn: 0, + getClippedImageForLeftTurn: 0, + getClippedImageNoAngle: 0, +}; // 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 USE_CACHING = true; + +// 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,21 +87,32 @@ function patchRenderer(renderer) { * @param {number} pixelRatio Ratio of device pixels to css pixels. * @returns {void} */ -function renderStrokeMarks( +async function renderStrokeMarks( renderContext, pixelCoords, graphicSpacing, pointStyle, pixelRatio, - options + options, + geometryType, + feature, ) { + const startTime = performance.now(); 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); - patchRenderer(render); // 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. @@ -79,7 +125,9 @@ function renderStrokeMarks( graphicSpacing, pointStyle, pixelRatio, - options + options, + geometryType, + feature, ); }); return; @@ -96,14 +144,20 @@ function renderStrokeMarks( return; } + if (!ogImage.iconImage_) { + return; + } + let ogImageWidth = null; + let ogImageHeight = null; if (ogImage.imgSize_) { ogImageWidth = ogImage.imgSize_[0]; + ogImageHeight = ogImage.imgSize_[1]; } const gapSize = graphicSpacing * pixelRatio; - var splitPoints = splitLineString( + const splitPoints = splitLineString( new LineString(pixelCoords), gapSize, { @@ -111,74 +165,1135 @@ function renderStrokeMarks( extent: render.extent_, placement: options.placement, initialGap: options.initialGap, - graphicWidth: ogImageWidth - } + graphicWidth: ogImageWidth, + }, ); - // Not quite a deep clone, but deep cloning the properties we need for rendering. - // We do this to not affect all individually rendered images when adjusting some of them. - const deepCloneImage = (image) => { - const copy = image.clone(); - - copy.imgSize_ = structuredClone(image.imgSize_); - copy.iconImage_ = new image.iconImage_.__proto__.constructor( - image.iconImage_.image_, - image.iconImage_.src_, - [image.iconImage_.size_[0], image.iconImage_.size_[1]], - image.iconImage_.crossOrigin_, - image.iconImage_.imageState_, - image.iconImage_.color_ - ); + // 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 copy; + return; } - // This loop renders the individual splitPoints. - splitPoints.forEach((point) => { + const pointsDataToRender = []; + let currentGeometryCoordIndex = null; + for (let i = 0; i < splitPoints.length; i++) { + const point = splitPoints[i]; let customRender = render; - let image; - - /* This whole function has some adjustment solely for the case of the graphic being wider than a segment of the geometry. - * Whenever this case occurs, we change the width of the image that will be rendered for the respective split point. - * The condition for this case is as follows: `gapSize > segmentLength`, where gapSize is the graphic width in the pixel - * space that's used for the splitLineString computation (which produces the splitPoints array), and segmentLength is the - * length of the segment that might be too short for the graphic (same pixel space as `gapSize`). - * To adjust the image size, we need to apply the ratio of `segmentLength / gapSize` (short segment length / long graphic width) - * to the actual graphic size, since the graphic width is given in a different pixel space. The resulting value will be the - * length of the segment in the pixelspace of the graphic, making the graphic exactly as long as the segment. - * (i.e. if `segmentLength` is 2/3 of `gapSize`, we want the image to be rendered 2/3 as wide as the original) - * For the rendering to work correctly, we need to use a separate render object. Otherwise the image size will be applied to - * every rendered image, due to the static way the render object holds the size information. We create a deep copy of the - * render object for this purpose (only the properties that we require are properly deep copied). - */ - if (ogImage.iconImage_) { - image = deepCloneImage(ogImage); - - const hasSegmentLength = point.length > 3; - if (hasSegmentLength) { - const segmentLength = point[3]; - if (gapSize > segmentLength) { - const imageToSegmentRatio = (segmentLength / gapSize); - const newVal = ogImageWidth * imageToSegmentRatio; - image.iconImage_.size_[0] = newVal; - - customRender = toContext(renderContext); - patchRenderer(customRender); + 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; + // First straight after full left turn, so the SECOND one on that segment + const isFirstAfterLeftTurn = !isLeftTurn + && !isRightTurn + && !isFirstOfSegment // First on segment IS the (second half) of the left turn + && i > 1 + && splitPoints[i - 1].isLeftTurn; + const isRegularButShortened = !isRightTurn + && !isLeftTurn + && !isFirstAfterLeftTurn + && point.segmentLength !== null && point.segmentLength !== undefined; + + point.isRightTurn = isRightTurn; + point.isLeftTurn = isLeftTurn; + point.isFirstOfSegment = isFirstOfSegment; + point.isFirstOfGeometry = isFirstOfGeometry; + point.isFirstAfterLeftTurn = isFirstAfterLeftTurn; + point.isRegularButShortened = isRegularButShortened; + + let newPointsDataToRender; + if (isRightTurn) { + newPointsDataToRender = await handleRightTurn({ + currentGeometryCoordIndex: currentGeometryCoordIndex, + pointsDataToRender: pointsDataToRender, + pImage: ogImage, + pImageWidth: ogImageWidth, + pImageHeight: ogImageHeight, + pRenderContext: renderContext, + pPixelRatio: pixelRatio, + ogImageWidth: ogImageWidth, + ogImageHeight: ogImageHeight, + splitPoint: point, + gapSize: gapSize, + onlyDoGap: false, + involvedGeometryCoords: { + coordOnFirstLine: pixelCoords[currentGeometryCoordIndex - 1], + intersectCoord: pixelCoords[currentGeometryCoordIndex], + coordOnSecondLine: pixelCoords[currentGeometryCoordIndex + 1], + }, + }); + } + else if (isLeftTurn) { + const leftTurnResult = await handleLeftTurn({ + i, + pixelCoords, + splitPoints, + point, + currentGeometryCoordIndex, + ogImageWidth, + ogImageHeight, + isFirstOfSegment, + ogImage, + renderContext, + pixelRatio, + pointsDataToRender, + gapSize, + image, + renderCoords, + customRender, + }); + customRender = leftTurnResult.customRender; + image = leftTurnResult.image; + renderCoords = leftTurnResult.renderCoords; + newPointsDataToRender = [leftTurnResult.newPointDataToRender]; + } else if (isFirstAfterLeftTurn) { + let clippedSrc; + 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) { + // const cutLength = ogImageWidth - distanceToPreviousSpPoint; + const cutLength = distanceToPreviousSpPoint; + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength, + cutInFront: true, + cutOnBothEnds: false, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } else { + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + // No info -> don't clip + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); } + } else { + const cutLength = point.segmentLength; + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength, + cutInFront: false, + cutOnBothEnds: true, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); } + const imageAnchor = [0.5, 0.5]; + image = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: imageAnchor, + }); + // image = new Icon({ + // src: clippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // image.getImage(pixelRatio).src = clippedSrc; + + newPointsDataToRender = [ + { + // ignore: true, + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; + } else if (isRegularButShortened) { + //image = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = image.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + const cutRatio = point.segmentLength / gapSize; + const cutLength = cutRatio * ogImageWidth; + const clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength: point.isFirstOfGeometry + ? ogImageWidth - cutLength + : cutLength, + cutInFront: false, + cutOnBothEnds: point.isFirstOfGeometry, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const imageAnchor = point.isFirstOfGeometry + ? [0.5, 0.5] + : [0, 0.5]; + image = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: imageAnchor, + }); + // image = new Icon({ + // src: clippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // image.getImage(pixelRatio).src = clippedSrc; + + newPointsDataToRender = [ + { + // ignore: true, + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; } else { - image = ogImage; + // Unchanged render + newPointsDataToRender = [ + { + // ignore: true, + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; + } + + // Polygon closing handling + const isPolygon = geometryType.includes('olygon'); + if (isPolygon) { + const isLastSplitPoint = i === splitPoints.length - 1; + if (isLastSplitPoint) { + const hasAdditionalPixelCoord = point.startingGeometryCoordIndex === pixelCoords.length - 2; + if (hasAdditionalPixelCoord) { + const nextPixelIsClosingPoint = point.startingGeometryCoordIndex !== 0 + && pixelCoords[point.startingGeometryCoordIndex + 1][0] === pixelCoords[0][0] + && pixelCoords[point.startingGeometryCoordIndex + 1][1] === pixelCoords[0][1]; + if (nextPixelIsClosingPoint) { + const lastSplitPoint = point; + const firstSplitPoint = splitPoints[0]; + const endOfPolygonIsRightTurn = getIsRightTurn( + pixelCoords[lastSplitPoint.startingGeometryCoordIndex], + pixelCoords[lastSplitPoint.startingGeometryCoordIndex + 1], // Is the same as [0] + pixelCoords[1], + ); + if (endOfPolygonIsRightTurn) { + const endOfPolygonGapFillRenderData = await handleRightTurn({ + currentGeometryCoordIndex: firstSplitPoint.startingGeometryCoordIndex, + pointsDataToRender: pointsDataToRender, + pImage: ogImage, + pImageWidth: ogImageWidth, + pImageHeight: ogImageHeight, + pRenderContext: renderContext, + pPixelRatio: pixelRatio, + ogImageWidth: ogImageWidth, + ogImageHeight: ogImageHeight, + splitPoint: firstSplitPoint, + gapSize: gapSize, + onlyDoGap: true, + involvedGeometryCoords: { + coordOnFirstLine: pixelCoords[lastSplitPoint.startingGeometryCoordIndex], + intersectCoord: pixelCoords[0], + coordOnSecondLine: pixelCoords[1], + }, + }); + newPointsDataToRender.push(endOfPolygonGapFillRenderData[0]); + newPointsDataToRender.push(endOfPolygonGapFillRenderData[1]); + } + } + } + } } + // end/ Polygon closing handling + + 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, + }); + }); + + perfMetrics.renderStrokeMarks += performance.now() - startTime; + // console.log(performance.now(), 'Performance metrics (ms):', perfMetrics); + perfMetrics = { + renderStrokeMarks: 0, + handleRightTurn: 0, + handleLeftTurn: 0, + getClippedImageForRightTurn: 0, + getClippedImageForLeftTurn: 0, + getClippedImageNoAngle: 0, + }; +} + +async function handleRightTurn(options) { + const startTime = performance.now(); + const currentGeometryCoordIndex = options.currentGeometryCoordIndex; + const pImage = options.pImage; + const pImageWidth = options.pImageWidth; + const pImageHeight = options.pImageHeight; + const pRenderContext = options.pRenderContext; + const pPixelRatio = options.pPixelRatio; + 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 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]; + + //let gapCloserImage = await deepCloneImage(pImage); + + // 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; + } + + let gapCloserImage = new Image(); + gapCloserImage.src = pImage.iconImage_.src_; + document.body.appendChild(gapCloserImage); + + const clippedSrc = await getClippedImageForRightTurn({ + img: gapCloserImage, + clipInfo: gapCloserPoint, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const imageAnchor = gapCloserPoint.isFirst + ? [0, 0.5] + : [1, 0.5]; + gapCloserImage = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [pImageWidth, pImageHeight], + scale: pImage.getScale(), + anchor: imageAnchor, + }); + // gapCloserImage = new Icon({ + // src: clippedSrc, + // imgSize: [pImageWidth, pImageHeight], + // scale: pImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // gapCloserImage.getImage(pPixelRatio).src = clippedSrc; - var splitPointAngle = image.getRotation() + point[2]; - customRender.setImageStyle2(image, splitPointAngle); - const pointToDraw = new Point([point[0] / pixelRatio, point[1] / pixelRatio]); - customRender.drawPoint(pointToDraw); + const gapCloserRenderer = toContext(pRenderContext); + + gapCloserRenderPoints.push({ + image: gapCloserImage, + angle: gapCloserPoint.angle, + coords: gapCloserPoint.intersectCoords, + rendererToUse: gapCloserRenderer, + isFirstOfRightTurn: gapCloserPoint.isFirst, + isSecondOfRightTurn: !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 clonedImage = await deepCloneImage(pImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = pImage.iconImage_.src_; + document.body.appendChild(img); + + const nextSegmentClippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: nextSegmentClipInfo, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const nextSegmentImageAnchor = [0, 0.5]; + const nextSegmentImage = createOlIconWithDataURL({ + src: nextSegmentClippedSrc, + imgSize: [pImageWidth, pImageHeight], + scale: pImage.getScale(), + anchor: nextSegmentImageAnchor, + }); + // const nextSegmentImage = new Icon({ + // src: nextSegmentClippedSrc, + // imgSize: [pImageWidth, pImageHeight], + // scale: pImage.getScale(), + // anchor: nextSegmentImageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // nextSegmentImage.getImage(pPixelRatio).src = nextSegmentClippedSrc; + + const nextSegmentRenderer = toContext(pRenderContext); + + const nextSegmentRenderPoint = { + // ignore: true, + image: nextSegmentImage, + angle: gapCloserPointData.backwardPoint.angle, + coords: gapCloserPointData.backwardPoint.intersectCoords, + rendererToUse: nextSegmentRenderer, + geometryCoordIndex: currentGeometryCoordIndex, + isFirstAfterRightTurn: true, + isClipped: nextSegmentCutRatio < 1, + clippedAtLength: nextSegmentCutLength, + }; + // \2 - finished + + const result = [ + ...gapCloserRenderPoints, + nextSegmentRenderPoint, + ]; + perfMetrics.handleRightTurn += performance.now() - startTime; + return result; +} + +async function handleLeftTurn(options) { + const startTime = performance.now(); + const pixelCoords = options.pixelCoords; + const point = options.point; + const currentGeometryCoordIndex = options.currentGeometryCoordIndex; + const ogImageWidth = options.ogImageWidth; + const ogImageHeight = options.ogImageHeight; + const ogImage = options.ogImage; + const renderContext = options.renderContext; + const pixelRatio = options.pixelRatio; + const pointsDataToRender = options.pointsDataToRender; + const gapSize = options.gapSize; + + /* 1 - Second half of the left turn (current split point) */ + // Prepare current split point image data (first on new geometry segment) + 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, // This is the first on the new segment, so the *second* half of the corner + isRightTurn: false, + cutRatio: cutLength / gapSize, + cutHeight: 0.5 * ogImageHeight, + cutAngle: cutAngle, + }; + + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + const clippedSrc = await getClippedImageForLeftTurn({ + img: img, + clipInfo: clipInfo, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + + const imageAnchor = [0.5, 0.5]; + const image = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: imageAnchor, + }); + // const image = new Icon({ + // src: clippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // image.getImage(pixelRatio).src = clippedSrc; + const renderCoords = point.splitPointCoords; + /* /1 */ + + /* 2 - First half of the left turn (adjust previous split point render data) */ + // (We can only do this in retrospect because we track this by the first splitpoint on the *next* segment) + const firstHalfOfLeftTurnRenderData = pointsDataToRender[pointsDataToRender.length - 1]; + + const firstHalfOfLeftTurnCutRatio = firstHalfOfLeftTurnRenderData.clippedAtLength / ogImageWidth; + const adjustingClipInfo = { + isFirst: true, + isRightTurn: false, + cutRatio: firstHalfOfLeftTurnCutRatio, + cutHeight: 0.5 * ogImageHeight, + cutAngle: cutAngle, // CutAngle is the same as before, since we're doing "half corner" for both. + }; + //const clonedLeftImage = await deepCloneImage(firstHalfOfLeftTurnRenderData.image); + const imgLeft = new Image(); + // imgLeft.src = clonedLeftImage.getSrc(); + imgLeft.src = firstHalfOfLeftTurnRenderData.image.iconImage_.src_; + document.body.appendChild(imgLeft); + const firstHalfOfLeftTurnClippedSrc = await getClippedImageForLeftTurn({ + // We use the previous image as a base here because sometimes they are already clipped on the other side, which we don't want to lose. + img: imgLeft, + clipInfo: adjustingClipInfo, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const firstHalfOfLeftTurnImageAnchor = firstHalfOfLeftTurnRenderData.fromSplitPoint.isFirstOfGeometry + ? [0.5, 0.5] + : firstHalfOfLeftTurnRenderData.image.anchor_; + firstHalfOfLeftTurnRenderData.image = createOlIconWithDataURL({ + src: firstHalfOfLeftTurnClippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: firstHalfOfLeftTurnImageAnchor, + }); + // firstHalfOfLeftTurnRenderData.image = new Icon({ + // src: firstHalfOfLeftTurnClippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: firstHalfOfLeftTurnImageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // firstHalfOfLeftTurnRenderData.image.getImage(pixelRatio).src = firstHalfOfLeftTurnClippedSrc; + // firstHalfOfLeftTurnRenderData.ignore = true; + /* /2 */ + + /* 3 + * We then also check the one BEFORE the first half of the left turn. + * -> The first half of the left turn is the last split point on the segment. + * The one before that, if rendered as the full graphic, might however also cause problems and be visible past the cut-off area, + * because the two last splitpoints on a segment are sometimes very close to each other. + * -> We therefore go back to that renderPoint and adjust it. + * */ + const hasRenderDataBeforeTurnOnSameSegment = pointsDataToRender.length - 2 >= 0 + && pointsDataToRender[pointsDataToRender.length - 2].geometryCoordIndex === firstHalfOfLeftTurnRenderData.geometryCoordIndex; + if (hasRenderDataBeforeTurnOnSameSegment) { + const lastRenderDataBeforeTurn = pointsDataToRender[pointsDataToRender.length - 2]; + + const lastRenderDataBeforeTurnCutRatio = calculatePointsDistance(lastRenderDataBeforeTurn.coords, point.splitPointCoords) / gapSize; + const lastRenderDataBeforeTurnCutLength = lastRenderDataBeforeTurnCutRatio * ogImageWidth; + //const clonedLastImage = await deepCloneImage(lastRenderDataBeforeTurn.image); + const img = new Image(); + // img.src = clonedLastImage.getSrc(); + img.src = lastRenderDataBeforeTurn.image.iconImage_.src_; + document.body.appendChild(img); + const lastRenderDataBeforeTurnClippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength: lastRenderDataBeforeTurnCutLength, + cutInFront: false, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + lastRenderDataBeforeTurn.image = createOlIconWithDataURL({ + src: lastRenderDataBeforeTurnClippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: lastRenderDataBeforeTurn.image.anchor_, + }); + // lastRenderDataBeforeTurn.image = new Icon({ + // src: lastRenderDataBeforeTurnClippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: lastRenderDataBeforeTurn.image.anchor_, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // lastRenderDataBeforeTurn.image.getImage(pixelRatio).src = lastRenderDataBeforeTurnClippedSrc; + // lastRenderDataBeforeTurn.ignore = true; + } + /* /3 */ + + const customRender = toContext(renderContext); + + const result = { + customRender: customRender, + image: image, + renderCoords: renderCoords, + newPointDataToRender: { + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + }; + perfMetrics.handleLeftTurn += performance.now() - startTime; + return result; +} + +function getClippedImageForRightTurn(options) { + const startTime = performance.now(); + 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'); + 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, + }); + if (USE_CACHING && clipInfoHashToBase64.rightTurn.has(clipInfoHashCode)) { + perfMetrics.getClippedImageForRightTurn += performance.now() - startTime; + return res(clipInfoHashToBase64.rightTurn.get(clipInfoHashCode).base64); + } + + ctx.save(); + ctx.beginPath(); + + if (clipInfo.isFirst) { + ctx.moveTo(0, 0); + ctx.lineTo(cutLength, 0); + const angledX = cutLength + Math.cos(Math.PI - clipInfo.cutAngle) * canvasDiagonal; + const angledY = Math.sin(clipInfo.cutAngle) * canvasDiagonal; + ctx.lineTo(angledX, angledY); + ctx.lineTo(0, canvas.height); + ctx.closePath(); + ctx.clip(); + } else { + ctx.moveTo(canvasWidth, 0); + ctx.lineTo(canvasWidth - cutLength, 0); + const angledX = canvasWidth - cutLength + Math.cos(clipInfo.cutAngle) * canvasDiagonal; + const angledY = Math.sin(clipInfo.cutAngle) * canvasDiagonal; + ctx.lineTo(angledX, angledY); + ctx.lineTo(canvasWidth, canvasHeight); + 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 + clipInfoHashToBase64.rightTurn.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + + perfMetrics.getClippedImageForRightTurn += performance.now() - startTime; + 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.rightTurn.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + + perfMetrics.getClippedImageForRightTurn += performance.now() - startTime; + res(result); + }; + } + }); +} + +function getClippedImageForLeftTurn(options) { + const startTime = performance.now(); + 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); // This is the max distance within the canvas + + const cutLength = clipInfo.cutRatio + ? clipInfo.cutRatio * canvas.width + : undefined; + + const clipInfoHashCode = getHashCode({ + cutAngle: clipInfo.cutAngle, + cutLength: cutLength, + canvasDiagonal: canvasDiagonal, + isFirst: clipInfo.isFirst, + img: img.src, + }); + if (USE_CACHING && clipInfoHashToBase64.leftTurn.has(clipInfoHashCode)) { + perfMetrics.getClippedImageForLeftTurn += performance.now() - startTime; + 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)) { + // We don't cache empty canvasses + clipInfoHashToBase64.leftTurn.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + + perfMetrics.getClippedImageForLeftTurn += performance.now() - startTime; + + 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.leftTurn.set(clipInfoHashCode, { + base64: result, + clipInfo: clipInfo, + }); + } + + perfMetrics.getClippedImageForLeftTurn += performance.now() - startTime; + + res(result); + }; + } + }); +} + +function getClippedImageNoAngle(options) { + const startTime = performance.now(); + 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)) { + perfMetrics.getClippedImageNoAngle += performance.now() - startTime; + 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, + }); + } + + perfMetrics.getClippedImageNoAngle += performance.now() - startTime; + + 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, + }); + } + + perfMetrics.getClippedImageNoAngle += performance.now() - startTime; + + res(result); + }; + } }); } /** - * Create a renderer function for renderining GraphicStroke marks + * 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); +} + +// Not quite a deep clone, but deep cloning the properties we need for rendering. +// We do this to not affect all individually rendered images when adjusting some of them. +async function deepCloneImage(image) { + + return new Promise((res, _) => { + if (image.getImage().complete) { + const copy = image.clone(); + + copy.imgSize_ = structuredClone(image.imgSize_); + copy.iconImage_ = new image.iconImage_.__proto__.constructor( + image.iconImage_.image_, + image.iconImage_.src_, + [image.iconImage_.size_[0], image.iconImage_.size_[1]], + image.iconImage_.crossOrigin_, + image.iconImage_.imageState_, + image.iconImage_.color_, + ); + + res(copy); + } else { + image.getImage().onload = () => { + const copy = image.clone(); + + copy.imgSize_ = structuredClone(image.imgSize_); + copy.iconImage_ = new image.iconImage_.__proto__.constructor( + image.iconImage_.image_, + image.iconImage_.src_, + [image.iconImage_.size_[0], image.iconImage_.size_[1]], + image.iconImage_.crossOrigin_, + image.iconImage_.imageState_, + image.iconImage_.color_, + ); + + res(copy); + }; + } + }); +} + +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; +} + +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); + +} + +/** + * 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; + + const icon = new Icon({ + img: tempImg, + imgSize: imgSize, + 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. @@ -188,7 +1303,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.', ); } @@ -209,7 +1324,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; } @@ -227,7 +1343,7 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { const pointStyle = getPointStyle( graphicstroke, renderState.feature, - getProperty + getProperty, ); // Calculate graphic spacing. @@ -241,8 +1357,8 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { graphicSizeExpression, renderState.feature, getProperty, - defaultGraphicSize - ) + defaultGraphicSize, + ), ); const graphicSpacing = calculateGraphicSpacing(linesymbolizer, graphicSize); @@ -254,7 +1370,9 @@ export function getGraphicStrokeRenderer(linesymbolizer, getProperty) { graphicSpacing, pointStyle, pixelRatio, - options + options, + geometryType, + renderState.feature, ); }; } @@ -269,7 +1387,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.', ); } From 2e0be691a1dbf171f926f3832a8330bb29b5928d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Fri, 26 Sep 2025 16:44:45 +0200 Subject: [PATCH 13/27] v0.6.0. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d416c511..7f1827a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@disy/sldreader", - "version": "0.5.4", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@disy/sldreader", - "version": "0.5.4", + "version": "0.6.0", "license": "ISC", "devDependencies": { "@babel/core": "^7.28.4", diff --git a/package.json b/package.json index 86b0ffb8..f8ab7bb1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.5.4", + "version": "0.6.0", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ From 43a81e4508688d8ced23f30831a287dc0c41dd52 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Thu, 11 Dec 2025 15:10:09 +0100 Subject: [PATCH 14/27] LD659 - fix for ol10 --- rollup.config.mjs | 16 +++++++++++++++- src/styles/graphicStrokeStyle.js | 14 +++++++++----- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 7429a56b..fb11a842 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -53,7 +53,21 @@ export default { plugins: [ babel({ babelHelpers: 'bundled', - presets: ['@babel/preset-env'] + // 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/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 27e96e4f..4f6d696e 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -150,9 +150,9 @@ async function renderStrokeMarks( let ogImageWidth = null; let ogImageHeight = null; - if (ogImage.imgSize_) { - ogImageWidth = ogImage.imgSize_[0]; - ogImageHeight = ogImage.imgSize_[1]; + if (ogImage.getSize()) { + ogImageWidth = ogImage.getSize()[0]; + ogImageHeight = ogImage.getSize()[1]; } const gapSize = graphicSpacing * pixelRatio; @@ -501,7 +501,6 @@ async function handleRightTurn(options) { const pImageWidth = options.pImageWidth; const pImageHeight = options.pImageHeight; const pRenderContext = options.pRenderContext; - const pPixelRatio = options.pPixelRatio; const ogImageWidth = options.ogImageWidth; const ogImageHeight = options.ogImageHeight; const splitPoint = options.splitPoint; @@ -1279,10 +1278,15 @@ function createOlIconWithDataURL(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, + imgSize: imgSize, + size: imgSize, // OL10 needs size when img is provided scale: scale, anchor: anchor, anchorXUnits: 'fraction', From 27db77a11623acd7e0280a1e3b6d8f3de0834969 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Thu, 11 Dec 2025 15:21:17 +0100 Subject: [PATCH 15/27] v0.7.0. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7f1827a2..50f042b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@disy/sldreader", - "version": "0.6.0", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@disy/sldreader", - "version": "0.6.0", + "version": "0.7.0", "license": "ISC", "devDependencies": { "@babel/core": "^7.28.4", diff --git a/package.json b/package.json index f8ab7bb1..b21fe2f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.6.0", + "version": "0.7.0", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ From 4947d9c6b266546db8e1261981791b539ada0d07 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 10:29:42 +0100 Subject: [PATCH 16/27] LD681 - minor refactoring --- src/styles/graphicStrokeStyle.js | 316 ++++++++++++++++++------------- 1 file changed, 187 insertions(+), 129 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 4f6d696e..414a4eca 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -268,138 +268,33 @@ async function renderStrokeMarks( renderCoords = leftTurnResult.renderCoords; newPointsDataToRender = [leftTurnResult.newPointDataToRender]; } else if (isFirstAfterLeftTurn) { - let clippedSrc; - 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) { - // const cutLength = ogImageWidth - distanceToPreviousSpPoint; - const cutLength = distanceToPreviousSpPoint; - //const clonedImage = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - cutLength, - cutInFront: true, - cutOnBothEnds: false, - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - } else { - //const clonedImage = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - // No info -> don't clip - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - } - } else { - const cutLength = point.segmentLength; - //const clonedImage = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - cutLength, - cutInFront: false, - cutOnBothEnds: true, - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - } - const imageAnchor = [0.5, 0.5]; - image = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: imageAnchor, + newPointsDataToRender = await handleFirstAfterLeftTurn({ + i, + point, + splitPoints, + gapSize, + ogImageWidth, + ogImageHeight, + ogImage, + pixelRatio, + image, + renderCoords, + customRender, + currentGeometryCoordIndex, }); - // image = new Icon({ - // src: clippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // image.getImage(pixelRatio).src = clippedSrc; - - newPointsDataToRender = [ - { - // ignore: true, - image: image, - angle: point.angle, - coords: renderCoords, - rendererToUse: customRender, - geometryCoordIndex: currentGeometryCoordIndex, - }, - ]; } else if (isRegularButShortened) { - //image = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = image.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - const cutRatio = point.segmentLength / gapSize; - const cutLength = cutRatio * ogImageWidth; - const clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - cutLength: point.isFirstOfGeometry - ? ogImageWidth - cutLength - : cutLength, - cutInFront: false, - cutOnBothEnds: point.isFirstOfGeometry, - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - const imageAnchor = point.isFirstOfGeometry - ? [0.5, 0.5] - : [0, 0.5]; - image = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: imageAnchor, + newPointsDataToRender = await handleRegularButShortened({ + point, + gapSize, + ogImageWidth, + ogImageHeight, + ogImage, + pixelRatio, + image, + renderCoords, + customRender, + currentGeometryCoordIndex, }); - // image = new Icon({ - // src: clippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // image.getImage(pixelRatio).src = clippedSrc; - - newPointsDataToRender = [ - { - // ignore: true, - image: image, - angle: point.angle, - coords: renderCoords, - rendererToUse: customRender, - geometryCoordIndex: currentGeometryCoordIndex, - }, - ]; } else { // Unchanged render newPointsDataToRender = [ @@ -636,6 +531,169 @@ async function handleRightTurn(options) { return result; } +async function handleFirstAfterLeftTurn(options) { + const i = options.i; + const point = options.point; + const splitPoints = options.splitPoints; + const gapSize = options.gapSize; + const ogImageWidth = options.ogImageWidth; + const ogImageHeight = options.ogImageHeight; + const ogImage = options.ogImage; + const pixelRatio = options.pixelRatio; + let image = options.image; + const renderCoords = options.renderCoords; + const customRender = options.customRender; + const currentGeometryCoordIndex = options.currentGeometryCoordIndex; + + let clippedSrc; + 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) { + // const cutLength = ogImageWidth - distanceToPreviousSpPoint; + const cutLength = distanceToPreviousSpPoint; + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength, + cutInFront: true, + cutOnBothEnds: false, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } else { + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + // No info -> don't clip + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } + } else { + const cutLength = point.segmentLength; + //const clonedImage = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = clonedImage.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength, + cutInFront: false, + cutOnBothEnds: true, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } + const imageAnchor = [0.5, 0.5]; + image = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: imageAnchor, + }); + // image = new Icon({ + // src: clippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // image.getImage(pixelRatio).src = clippedSrc; + + const result = [ + { + // ignore: true, + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; + return result; +} + +async function handleRegularButShortened(options) { + const point = options.point; + const gapSize = options.gapSize; + const ogImageWidth = options.ogImageWidth; + const ogImageHeight = options.ogImageHeight; + const ogImage = options.ogImage; + const pixelRatio = options.pixelRatio; + let image = options.image; + const renderCoords = options.renderCoords; + const customRender = options.customRender; + const currentGeometryCoordIndex = options.currentGeometryCoordIndex; + + //image = await deepCloneImage(ogImage); + const img = new Image(); + // img.src = image.getSrc(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + const cutRatio = point.segmentLength / gapSize; + const cutLength = cutRatio * ogImageWidth; + const clippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: { + cutLength: point.isFirstOfGeometry + ? ogImageWidth - cutLength + : cutLength, + cutInFront: false, + cutOnBothEnds: point.isFirstOfGeometry, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const imageAnchor = point.isFirstOfGeometry + ? [0.5, 0.5] + : [0, 0.5]; + image = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: imageAnchor, + }); + // image = new Icon({ + // src: clippedSrc, + // imgSize: [ogImageWidth, ogImageHeight], + // scale: ogImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // image.getImage(pixelRatio).src = clippedSrc; + + const result = [ + { + // ignore: true, + image: image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; + return result; +} + async function handleLeftTurn(options) { const startTime = performance.now(); const pixelCoords = options.pixelCoords; From 4adbf3c478d0295a908253625ecdba4e36d5c694 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 13:04:41 +0100 Subject: [PATCH 17/27] LD681 - WIP, treating left turn like right turn --- src/styles/graphicStrokeStyle.js | 598 ++++++++++--------------------- 1 file changed, 196 insertions(+), 402 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 414a4eca..70c19aeb 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -14,22 +14,9 @@ import { calculateGraphicSpacing, getInitialGapSize } from './styleUtils'; import { splitLineString, getGapCloserPoints, - calculatePointsDistance, - angleInRadiansAtB, - getMirroredCoords, getIsRightTurn, } from './geometryCalcs'; -// Performance tracking -let perfMetrics = { - renderStrokeMarks: 0, - handleRightTurn: 0, - handleLeftTurn: 0, - getClippedImageForRightTurn: 0, - getClippedImageForLeftTurn: 0, - getClippedImageNoAngle: 0, -}; - // A flag to prevent multiple renderer patches. let rendererPatched = false; @@ -97,7 +84,6 @@ async function renderStrokeMarks( geometryType, feature, ) { - const startTime = performance.now(); if (!pixelCoords) { return; } @@ -204,22 +190,14 @@ async function renderStrokeMarks( const isLeftTurn = !isFirstOfGeometry && isFirstOfSegment && !isRightTurn; - // First straight after full left turn, so the SECOND one on that segment - const isFirstAfterLeftTurn = !isLeftTurn - && !isRightTurn - && !isFirstOfSegment // First on segment IS the (second half) of the left turn - && i > 1 - && splitPoints[i - 1].isLeftTurn; const isRegularButShortened = !isRightTurn && !isLeftTurn - && !isFirstAfterLeftTurn && point.segmentLength !== null && point.segmentLength !== undefined; point.isRightTurn = isRightTurn; point.isLeftTurn = isLeftTurn; point.isFirstOfSegment = isFirstOfSegment; point.isFirstOfGeometry = isFirstOfGeometry; - point.isFirstAfterLeftTurn = isFirstAfterLeftTurn; point.isRegularButShortened = isRegularButShortened; let newPointsDataToRender; @@ -245,44 +223,27 @@ async function renderStrokeMarks( }); } else if (isLeftTurn) { - const leftTurnResult = await handleLeftTurn({ - i, - pixelCoords, - splitPoints, - point, - currentGeometryCoordIndex, - ogImageWidth, - ogImageHeight, - isFirstOfSegment, - ogImage, - renderContext, - pixelRatio, - pointsDataToRender, - gapSize, - image, - renderCoords, - customRender, - }); - customRender = leftTurnResult.customRender; - image = leftTurnResult.image; - renderCoords = leftTurnResult.renderCoords; - newPointsDataToRender = [leftTurnResult.newPointDataToRender]; - } else if (isFirstAfterLeftTurn) { - newPointsDataToRender = await handleFirstAfterLeftTurn({ - i, - point, - splitPoints, + newPointsDataToRender = await handleLeftTurn({ + currentGeometryCoordIndex: currentGeometryCoordIndex, + pointsDataToRender: pointsDataToRender, + pImage: ogImage, + pImageWidth: ogImageWidth, + pImageHeight: ogImageHeight, + pRenderContext: renderContext, + pPixelRatio: pixelRatio, + ogImageWidth: ogImageWidth, + ogImageHeight: ogImageHeight, + splitPoint: point, gapSize, - ogImageWidth, - ogImageHeight, - ogImage, - pixelRatio, - image, - renderCoords, - customRender, - currentGeometryCoordIndex, + onlyDoGap: false, + involvedGeometryCoords: { + coordOnFirstLine: pixelCoords[currentGeometryCoordIndex - 1], + intersectCoord: pixelCoords[currentGeometryCoordIndex], + coordOnSecondLine: pixelCoords[currentGeometryCoordIndex + 1], + }, }); - } else if (isRegularButShortened) { + } + else if (isRegularButShortened) { newPointsDataToRender = await handleRegularButShortened({ point, gapSize, @@ -295,7 +256,8 @@ async function renderStrokeMarks( customRender, currentGeometryCoordIndex, }); - } else { + } + else { // Unchanged render newPointsDataToRender = [ { @@ -327,8 +289,27 @@ async function renderStrokeMarks( pixelCoords[lastSplitPoint.startingGeometryCoordIndex + 1], // Is the same as [0] pixelCoords[1], ); - if (endOfPolygonIsRightTurn) { - const endOfPolygonGapFillRenderData = await handleRightTurn({ + const endOfPolygonGapFillRenderData = endOfPolygonIsRightTurn + ? await handleRightTurn({ + currentGeometryCoordIndex: firstSplitPoint.startingGeometryCoordIndex, + pointsDataToRender: pointsDataToRender, + pImage: ogImage, + pImageWidth: ogImageWidth, + pImageHeight: ogImageHeight, + pRenderContext: renderContext, + pPixelRatio: pixelRatio, + ogImageWidth: ogImageWidth, + ogImageHeight: ogImageHeight, + splitPoint: firstSplitPoint, + gapSize: gapSize, + onlyDoGap: true, + involvedGeometryCoords: { + coordOnFirstLine: pixelCoords[lastSplitPoint.startingGeometryCoordIndex], + intersectCoord: pixelCoords[0], + coordOnSecondLine: pixelCoords[1], + }, + }) + : await handleLeftTurn({ currentGeometryCoordIndex: firstSplitPoint.startingGeometryCoordIndex, pointsDataToRender: pointsDataToRender, pImage: ogImage, @@ -347,9 +328,8 @@ async function renderStrokeMarks( coordOnSecondLine: pixelCoords[1], }, }); - newPointsDataToRender.push(endOfPolygonGapFillRenderData[0]); - newPointsDataToRender.push(endOfPolygonGapFillRenderData[1]); - } + newPointsDataToRender.push(endOfPolygonGapFillRenderData[0]); + newPointsDataToRender.push(endOfPolygonGapFillRenderData[1]); } } } @@ -376,21 +356,9 @@ async function renderStrokeMarks( feature, }); }); - - perfMetrics.renderStrokeMarks += performance.now() - startTime; - // console.log(performance.now(), 'Performance metrics (ms):', perfMetrics); - perfMetrics = { - renderStrokeMarks: 0, - handleRightTurn: 0, - handleLeftTurn: 0, - getClippedImageForRightTurn: 0, - getClippedImageForLeftTurn: 0, - getClippedImageNoAngle: 0, - }; } async function handleRightTurn(options) { - const startTime = performance.now(); const currentGeometryCoordIndex = options.currentGeometryCoordIndex; const pImage = options.pImage; const pImageWidth = options.pImageWidth; @@ -464,6 +432,7 @@ async function handleRightTurn(options) { isSecondOfRightTurn: !gapCloserPoint.isFirst, isClipped: true, }); + } // \1 - finished @@ -527,107 +496,6 @@ async function handleRightTurn(options) { ...gapCloserRenderPoints, nextSegmentRenderPoint, ]; - perfMetrics.handleRightTurn += performance.now() - startTime; - return result; -} - -async function handleFirstAfterLeftTurn(options) { - const i = options.i; - const point = options.point; - const splitPoints = options.splitPoints; - const gapSize = options.gapSize; - const ogImageWidth = options.ogImageWidth; - const ogImageHeight = options.ogImageHeight; - const ogImage = options.ogImage; - const pixelRatio = options.pixelRatio; - let image = options.image; - const renderCoords = options.renderCoords; - const customRender = options.customRender; - const currentGeometryCoordIndex = options.currentGeometryCoordIndex; - - let clippedSrc; - 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) { - // const cutLength = ogImageWidth - distanceToPreviousSpPoint; - const cutLength = distanceToPreviousSpPoint; - //const clonedImage = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - cutLength, - cutInFront: true, - cutOnBothEnds: false, - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - } else { - //const clonedImage = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - // No info -> don't clip - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - } - } else { - const cutLength = point.segmentLength; - //const clonedImage = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - cutLength, - cutInFront: false, - cutOnBothEnds: true, - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - } - const imageAnchor = [0.5, 0.5]; - image = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: imageAnchor, - }); - // image = new Icon({ - // src: clippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // image.getImage(pixelRatio).src = clippedSrc; - - const result = [ - { - // ignore: true, - image: image, - angle: point.angle, - coords: renderCoords, - rendererToUse: customRender, - geometryCoordIndex: currentGeometryCoordIndex, - }, - ]; return result; } @@ -695,184 +563,147 @@ async function handleRegularButShortened(options) { } async function handleLeftTurn(options) { - const startTime = performance.now(); - const pixelCoords = options.pixelCoords; - const point = options.point; 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 ogImage = options.ogImage; - const renderContext = options.renderContext; - const pixelRatio = options.pixelRatio; - const pointsDataToRender = options.pointsDataToRender; + const splitPoint = options.splitPoint; const gapSize = options.gapSize; + const onlyDoGap = options.onlyDoGap; + const involvedGeometryCoords = options.involvedGeometryCoords; - /* 1 - Second half of the left turn (current split point) */ - // Prepare current split point image data (first on new geometry segment) - 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, // This is the first on the new segment, so the *second* half of the corner - isRightTurn: false, - cutRatio: cutLength / gapSize, - cutHeight: 0.5 * ogImageHeight, - cutAngle: cutAngle, - }; + 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]; + + //let gapCloserImage = await deepCloneImage(pImage); + + // 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; + } + + let gapCloserImage = new Image(); + gapCloserImage.src = pImage.iconImage_.src_; + document.body.appendChild(gapCloserImage); + + const clippedSrc = await getClippedImageForLeftTurn({ + img: gapCloserImage, + clipInfo: gapCloserPoint, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const imageAnchor = gapCloserPoint.isFirst + ? [0, 0.5] + : [1, 0.5]; + gapCloserImage = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [pImageWidth, pImageHeight], + scale: pImage.getScale(), + anchor: imageAnchor, + }); + // gapCloserImage = new Icon({ + // src: clippedSrc, + // imgSize: [pImageWidth, pImageHeight], + // scale: pImage.getScale(), + // anchor: imageAnchor, + // anchorXUnits: 'fraction', + // anchorYUnits: 'fraction', + // }); + // gapCloserImage.getImage(pPixelRatio).src = clippedSrc; + + const gapCloserRenderer = toContext(pRenderContext); - //const clonedImage = await deepCloneImage(ogImage); + gapCloserRenderPoints.push({ + image: gapCloserImage, + angle: gapCloserPoint.angle, + coords: gapCloserPoint.intersectCoords, + rendererToUse: gapCloserRenderer, + isFirstOfLeftTurn: gapCloserPoint.isFirst, + isSecondOfLeftTurn: !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 clonedImage = await deepCloneImage(pImage); const img = new Image(); // img.src = clonedImage.getSrc(); - img.src = ogImage.iconImage_.src_; + img.src = pImage.iconImage_.src_; document.body.appendChild(img); - const clippedSrc = await getClippedImageForLeftTurn({ - img: img, - clipInfo: clipInfo, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - const imageAnchor = [0.5, 0.5]; - const image = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: imageAnchor, - }); - // const image = new Icon({ - // src: clippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // image.getImage(pixelRatio).src = clippedSrc; - const renderCoords = point.splitPointCoords; - /* /1 */ - - /* 2 - First half of the left turn (adjust previous split point render data) */ - // (We can only do this in retrospect because we track this by the first splitpoint on the *next* segment) - const firstHalfOfLeftTurnRenderData = pointsDataToRender[pointsDataToRender.length - 1]; - - const firstHalfOfLeftTurnCutRatio = firstHalfOfLeftTurnRenderData.clippedAtLength / ogImageWidth; - const adjustingClipInfo = { - isFirst: true, - isRightTurn: false, - cutRatio: firstHalfOfLeftTurnCutRatio, - cutHeight: 0.5 * ogImageHeight, - cutAngle: cutAngle, // CutAngle is the same as before, since we're doing "half corner" for both. - }; - //const clonedLeftImage = await deepCloneImage(firstHalfOfLeftTurnRenderData.image); - const imgLeft = new Image(); - // imgLeft.src = clonedLeftImage.getSrc(); - imgLeft.src = firstHalfOfLeftTurnRenderData.image.iconImage_.src_; - document.body.appendChild(imgLeft); - const firstHalfOfLeftTurnClippedSrc = await getClippedImageForLeftTurn({ - // We use the previous image as a base here because sometimes they are already clipped on the other side, which we don't want to lose. - img: imgLeft, - clipInfo: adjustingClipInfo, + const nextSegmentClippedSrc = await getClippedImageNoAngle({ + img: img, + clipInfo: nextSegmentClipInfo, canvasWidth: ogImageWidth, canvasHeight: ogImageHeight, }); - const firstHalfOfLeftTurnImageAnchor = firstHalfOfLeftTurnRenderData.fromSplitPoint.isFirstOfGeometry - ? [0.5, 0.5] - : firstHalfOfLeftTurnRenderData.image.anchor_; - firstHalfOfLeftTurnRenderData.image = createOlIconWithDataURL({ - src: firstHalfOfLeftTurnClippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: firstHalfOfLeftTurnImageAnchor, + const nextSegmentImageAnchor = [0, 0.5]; + const nextSegmentImage = createOlIconWithDataURL({ + src: nextSegmentClippedSrc, + imgSize: [pImageWidth, pImageHeight], + scale: pImage.getScale(), + anchor: nextSegmentImageAnchor, }); - // firstHalfOfLeftTurnRenderData.image = new Icon({ - // src: firstHalfOfLeftTurnClippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: firstHalfOfLeftTurnImageAnchor, + // const nextSegmentImage = new Icon({ + // src: nextSegmentClippedSrc, + // imgSize: [pImageWidth, pImageHeight], + // scale: pImage.getScale(), + // anchor: nextSegmentImageAnchor, // anchorXUnits: 'fraction', // anchorYUnits: 'fraction', // }); - // firstHalfOfLeftTurnRenderData.image.getImage(pixelRatio).src = firstHalfOfLeftTurnClippedSrc; - // firstHalfOfLeftTurnRenderData.ignore = true; - /* /2 */ - - /* 3 - * We then also check the one BEFORE the first half of the left turn. - * -> The first half of the left turn is the last split point on the segment. - * The one before that, if rendered as the full graphic, might however also cause problems and be visible past the cut-off area, - * because the two last splitpoints on a segment are sometimes very close to each other. - * -> We therefore go back to that renderPoint and adjust it. - * */ - const hasRenderDataBeforeTurnOnSameSegment = pointsDataToRender.length - 2 >= 0 - && pointsDataToRender[pointsDataToRender.length - 2].geometryCoordIndex === firstHalfOfLeftTurnRenderData.geometryCoordIndex; - if (hasRenderDataBeforeTurnOnSameSegment) { - const lastRenderDataBeforeTurn = pointsDataToRender[pointsDataToRender.length - 2]; - - const lastRenderDataBeforeTurnCutRatio = calculatePointsDistance(lastRenderDataBeforeTurn.coords, point.splitPointCoords) / gapSize; - const lastRenderDataBeforeTurnCutLength = lastRenderDataBeforeTurnCutRatio * ogImageWidth; - //const clonedLastImage = await deepCloneImage(lastRenderDataBeforeTurn.image); - const img = new Image(); - // img.src = clonedLastImage.getSrc(); - img.src = lastRenderDataBeforeTurn.image.iconImage_.src_; - document.body.appendChild(img); - const lastRenderDataBeforeTurnClippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: { - cutLength: lastRenderDataBeforeTurnCutLength, - cutInFront: false, - }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - lastRenderDataBeforeTurn.image = createOlIconWithDataURL({ - src: lastRenderDataBeforeTurnClippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: lastRenderDataBeforeTurn.image.anchor_, - }); - // lastRenderDataBeforeTurn.image = new Icon({ - // src: lastRenderDataBeforeTurnClippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: lastRenderDataBeforeTurn.image.anchor_, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // lastRenderDataBeforeTurn.image.getImage(pixelRatio).src = lastRenderDataBeforeTurnClippedSrc; - // lastRenderDataBeforeTurn.ignore = true; - } - /* /3 */ + // nextSegmentImage.getImage(pPixelRatio).src = nextSegmentClippedSrc; - const customRender = toContext(renderContext); + const nextSegmentRenderer = toContext(pRenderContext); - const result = { - customRender: customRender, - image: image, - renderCoords: renderCoords, - newPointDataToRender: { - image: image, - angle: point.angle, - coords: renderCoords, - rendererToUse: customRender, - geometryCoordIndex: currentGeometryCoordIndex, - }, + const nextSegmentRenderPoint = { + // ignore: true, + image: nextSegmentImage, + angle: gapCloserPointData.backwardPoint.angle, + coords: gapCloserPointData.backwardPoint.intersectCoords, + rendererToUse: nextSegmentRenderer, + geometryCoordIndex: currentGeometryCoordIndex, + isFirstAfterLeftTurn: true, + isClipped: nextSegmentCutRatio < 1, + clippedAtLength: nextSegmentCutLength, }; - perfMetrics.handleLeftTurn += performance.now() - startTime; + // \2 - finished + + const result = [ + ...gapCloserRenderPoints, + nextSegmentRenderPoint, + ]; return result; } function getClippedImageForRightTurn(options) { - const startTime = performance.now(); const img = options.img; const clipInfo = options.clipInfo; const canvasWidth = options.canvasWidth; @@ -899,7 +730,6 @@ function getClippedImageForRightTurn(options) { img: img.src, }); if (USE_CACHING && clipInfoHashToBase64.rightTurn.has(clipInfoHashCode)) { - perfMetrics.getClippedImageForRightTurn += performance.now() - startTime; return res(clipInfoHashToBase64.rightTurn.get(clipInfoHashCode).base64); } @@ -938,8 +768,6 @@ function getClippedImageForRightTurn(options) { clipInfo: clipInfo, }); } - - perfMetrics.getClippedImageForRightTurn += performance.now() - startTime; res(result); } else { img.onload = () => { @@ -954,8 +782,6 @@ function getClippedImageForRightTurn(options) { clipInfo: clipInfo, }); } - - perfMetrics.getClippedImageForRightTurn += performance.now() - startTime; res(result); }; } @@ -963,79 +789,57 @@ function getClippedImageForRightTurn(options) { } function getClippedImageForLeftTurn(options) { - const startTime = performance.now(); - const { - img, - clipInfo, - canvasWidth, - canvasHeight, - } = 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'); - const canvasDiagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2); // This is the max distance within the canvas - - const cutLength = clipInfo.cutRatio - ? clipInfo.cutRatio * canvas.width - : undefined; + 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: cutLength, - canvasDiagonal: canvasDiagonal, + cutLength: clipInfo.cutLength, + canvasDiagonal: clipInfo.canvasDiagonal, isFirst: clipInfo.isFirst, img: img.src, }); if (USE_CACHING && clipInfoHashToBase64.leftTurn.has(clipInfoHashCode)) { - perfMetrics.getClippedImageForLeftTurn += performance.now() - startTime; 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.save(); 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 (clipInfo.isFirst) { + ctx.moveTo(0, canvasHeight); + ctx.lineTo(cutLength, canvasHeight); + const angledX = cutLength + Math.cos(Math.PI - clipInfo.cutAngle) * canvasDiagonal; + const angledY = canvasHeight - Math.sin(clipInfo.cutAngle) * canvasDiagonal; + ctx.lineTo(angledX, angledY); + ctx.lineTo(0, 0); + ctx.closePath(); + ctx.clip(); + } else { + ctx.moveTo(canvasWidth, canvasHeight); + ctx.lineTo(canvasWidth - cutLength, canvasHeight); + const angledX = canvasWidth - cutLength + Math.cos(clipInfo.cutAngle) * canvasDiagonal; + const angledY = canvasHeight - Math.sin(clipInfo.cutAngle) * canvasDiagonal; + ctx.lineTo(angledX, angledY); + ctx.lineTo(canvasWidth, 0); + ctx.closePath(); + ctx.clip(); + } if (img.complete) { ctx.drawImage(img, 0, 0); @@ -1050,8 +854,6 @@ function getClippedImageForLeftTurn(options) { }); } - perfMetrics.getClippedImageForLeftTurn += performance.now() - startTime; - res(result); } else { img.onload = () => { @@ -1067,8 +869,6 @@ function getClippedImageForLeftTurn(options) { }); } - perfMetrics.getClippedImageForLeftTurn += performance.now() - startTime; - res(result); }; } @@ -1076,7 +876,6 @@ function getClippedImageForLeftTurn(options) { } function getClippedImageNoAngle(options) { - const startTime = performance.now(); const img = options.img; const clipInfo = options.clipInfo; const canvasWidth = options.canvasWidth; @@ -1104,7 +903,6 @@ function getClippedImageNoAngle(options) { img: img.src, }); if (USE_CACHING && clipInfoHashToBase64.straight.has(clipInfoHashCode)) { - perfMetrics.getClippedImageNoAngle += performance.now() - startTime; return res(clipInfoHashToBase64.straight.get(clipInfoHashCode).base64); } @@ -1133,8 +931,6 @@ function getClippedImageNoAngle(options) { }); } - perfMetrics.getClippedImageNoAngle += performance.now() - startTime; - res(result); } else { img.onload = () => { @@ -1150,8 +946,6 @@ function getClippedImageNoAngle(options) { }); } - perfMetrics.getClippedImageNoAngle += performance.now() - startTime; - res(result); }; } @@ -1343,7 +1137,7 @@ function createOlIconWithDataURL(options) { const icon = new Icon({ img: tempImg, - imgSize: imgSize, + imgSize: imgSize, size: imgSize, // OL10 needs size when img is provided scale: scale, anchor: anchor, From 62fe12640f7fcaff34c4261eb40e5647844d43d6 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 13:25:30 +0100 Subject: [PATCH 18/27] LD681 - WIP, refactoring of existing logic --- src/styles/graphicStrokeStyle.js | 310 +++++++------------------------ 1 file changed, 68 insertions(+), 242 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 70c19aeb..2b36af22 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -35,6 +35,27 @@ const clipInfoHashToBase64 = { }; 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; @@ -358,7 +379,24 @@ async function renderStrokeMarks( }); } +function getTurnDirectionConfig(turnDirection) { + const turnDirectionConfig = TURN_DIRECTION_CONFIG[turnDirection]; + + if (!turnDirectionConfig) { + throw new Error(`Unsupported turn direction: ${turnDirection}`); + } + + return turnDirectionConfig; +} + async function handleRightTurn(options) { + return handleTurn({ + ...options, + turnDirection: 'right', + }); +} + +async function handleTurn(options) { const currentGeometryCoordIndex = options.currentGeometryCoordIndex; const pImage = options.pImage; const pImageWidth = options.pImageWidth; @@ -370,6 +408,8 @@ async function handleRightTurn(options) { 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, @@ -396,11 +436,12 @@ async function handleRightTurn(options) { gapCloserImage.src = pImage.iconImage_.src_; document.body.appendChild(gapCloserImage); - const clippedSrc = await getClippedImageForRightTurn({ + const clippedSrc = await getClippedImageForTurn({ img: gapCloserImage, clipInfo: gapCloserPoint, canvasWidth: ogImageWidth, canvasHeight: ogImageHeight, + turnDirection, }); const imageAnchor = gapCloserPoint.isFirst ? [0, 0.5] @@ -428,11 +469,10 @@ async function handleRightTurn(options) { angle: gapCloserPoint.angle, coords: gapCloserPoint.intersectCoords, rendererToUse: gapCloserRenderer, - isFirstOfRightTurn: gapCloserPoint.isFirst, - isSecondOfRightTurn: !gapCloserPoint.isFirst, + [turnDirectionConfig.firstGapFlag]: gapCloserPoint.isFirst, + [turnDirectionConfig.secondGapFlag]: !gapCloserPoint.isFirst, isClipped: true, }); - } // \1 - finished @@ -486,7 +526,7 @@ async function handleRightTurn(options) { coords: gapCloserPointData.backwardPoint.intersectCoords, rendererToUse: nextSegmentRenderer, geometryCoordIndex: currentGeometryCoordIndex, - isFirstAfterRightTurn: true, + [turnDirectionConfig.nextSegmentFlag]: true, isClipped: nextSegmentCutRatio < 1, clippedAtLength: nextSegmentCutLength, }; @@ -563,151 +603,19 @@ async function handleRegularButShortened(options) { } async function handleLeftTurn(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 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]; - - //let gapCloserImage = await deepCloneImage(pImage); - - // 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; - } - - let gapCloserImage = new Image(); - gapCloserImage.src = pImage.iconImage_.src_; - document.body.appendChild(gapCloserImage); - - const clippedSrc = await getClippedImageForLeftTurn({ - img: gapCloserImage, - clipInfo: gapCloserPoint, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - const imageAnchor = gapCloserPoint.isFirst - ? [0, 0.5] - : [1, 0.5]; - gapCloserImage = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [pImageWidth, pImageHeight], - scale: pImage.getScale(), - anchor: imageAnchor, - }); - // gapCloserImage = new Icon({ - // src: clippedSrc, - // imgSize: [pImageWidth, pImageHeight], - // scale: pImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // gapCloserImage.getImage(pPixelRatio).src = clippedSrc; - - const gapCloserRenderer = toContext(pRenderContext); - - gapCloserRenderPoints.push({ - image: gapCloserImage, - angle: gapCloserPoint.angle, - coords: gapCloserPoint.intersectCoords, - rendererToUse: gapCloserRenderer, - isFirstOfLeftTurn: gapCloserPoint.isFirst, - isSecondOfLeftTurn: !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 clonedImage = await deepCloneImage(pImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = pImage.iconImage_.src_; - document.body.appendChild(img); - - const nextSegmentClippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: nextSegmentClipInfo, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - const nextSegmentImageAnchor = [0, 0.5]; - const nextSegmentImage = createOlIconWithDataURL({ - src: nextSegmentClippedSrc, - imgSize: [pImageWidth, pImageHeight], - scale: pImage.getScale(), - anchor: nextSegmentImageAnchor, + return handleTurn({ + ...options, + turnDirection: 'left', }); - // const nextSegmentImage = new Icon({ - // src: nextSegmentClippedSrc, - // imgSize: [pImageWidth, pImageHeight], - // scale: pImage.getScale(), - // anchor: nextSegmentImageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // nextSegmentImage.getImage(pPixelRatio).src = nextSegmentClippedSrc; - - const nextSegmentRenderer = toContext(pRenderContext); - - const nextSegmentRenderPoint = { - // ignore: true, - image: nextSegmentImage, - angle: gapCloserPointData.backwardPoint.angle, - coords: gapCloserPointData.backwardPoint.intersectCoords, - rendererToUse: nextSegmentRenderer, - geometryCoordIndex: currentGeometryCoordIndex, - isFirstAfterLeftTurn: true, - isClipped: nextSegmentCutRatio < 1, - clippedAtLength: nextSegmentCutLength, - }; - // \2 - finished - - const result = [ - ...gapCloserRenderPoints, - nextSegmentRenderPoint, - ]; - return result; } -function getClippedImageForRightTurn(options) { +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'); @@ -729,29 +637,34 @@ function getClippedImageForRightTurn(options) { isFirst: clipInfo.isFirst, img: img.src, }); - if (USE_CACHING && clipInfoHashToBase64.rightTurn.has(clipInfoHashCode)) { - return res(clipInfoHashToBase64.rightTurn.get(clipInfoHashCode).base64); + 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, 0); - ctx.lineTo(cutLength, 0); + ctx.moveTo(0, edgeY); + ctx.lineTo(cutLength, edgeY); const angledX = cutLength + Math.cos(Math.PI - clipInfo.cutAngle) * canvasDiagonal; - const angledY = Math.sin(clipInfo.cutAngle) * canvasDiagonal; + const angledY = edgeY + angledYOffset; ctx.lineTo(angledX, angledY); - ctx.lineTo(0, canvas.height); + ctx.lineTo(0, oppositeY); ctx.closePath(); ctx.clip(); } else { - ctx.moveTo(canvasWidth, 0); - ctx.lineTo(canvasWidth - cutLength, 0); + ctx.moveTo(canvasWidth, edgeY); + ctx.lineTo(canvasWidth - cutLength, edgeY); const angledX = canvasWidth - cutLength + Math.cos(clipInfo.cutAngle) * canvasDiagonal; - const angledY = Math.sin(clipInfo.cutAngle) * canvasDiagonal; + const angledY = edgeY + angledYOffset; ctx.lineTo(angledX, angledY); - ctx.lineTo(canvasWidth, canvasHeight); + ctx.lineTo(canvasWidth, oppositeY); ctx.closePath(); ctx.clip(); } @@ -763,7 +676,7 @@ function getClippedImageForRightTurn(options) { const result = canvas.toDataURL(); if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { // We don't cache empty canvasses - clipInfoHashToBase64.rightTurn.set(clipInfoHashCode, { + clipCache.set(clipInfoHashCode, { base64: result, clipInfo: clipInfo, }); @@ -777,7 +690,7 @@ function getClippedImageForRightTurn(options) { const result = canvas.toDataURL(); if (!isCanvasEmpty(result, canvasWidth, canvasHeight)) { // We don't cache empty canvasses - clipInfoHashToBase64.rightTurn.set(clipInfoHashCode, { + clipCache.set(clipInfoHashCode, { base64: result, clipInfo: clipInfo, }); @@ -788,93 +701,6 @@ function getClippedImageForRightTurn(options) { }); } -function getClippedImageForLeftTurn(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'); - 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, - }); - if (USE_CACHING && clipInfoHashToBase64.leftTurn.has(clipInfoHashCode)) { - return res(clipInfoHashToBase64.leftTurn.get(clipInfoHashCode).base64); - } - - ctx.save(); - ctx.beginPath(); - - if (clipInfo.isFirst) { - ctx.moveTo(0, canvasHeight); - ctx.lineTo(cutLength, canvasHeight); - const angledX = cutLength + Math.cos(Math.PI - clipInfo.cutAngle) * canvasDiagonal; - const angledY = canvasHeight - Math.sin(clipInfo.cutAngle) * canvasDiagonal; - ctx.lineTo(angledX, angledY); - ctx.lineTo(0, 0); - ctx.closePath(); - ctx.clip(); - } else { - ctx.moveTo(canvasWidth, canvasHeight); - ctx.lineTo(canvasWidth - cutLength, canvasHeight); - const angledX = canvasWidth - cutLength + Math.cos(clipInfo.cutAngle) * canvasDiagonal; - const angledY = canvasHeight - Math.sin(clipInfo.cutAngle) * canvasDiagonal; - ctx.lineTo(angledX, angledY); - ctx.lineTo(canvasWidth, 0); - 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 - clipInfoHashToBase64.leftTurn.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.leftTurn.set(clipInfoHashCode, { - base64: result, - clipInfo: clipInfo, - }); - } - - res(result); - }; - } - }); -} - function getClippedImageNoAngle(options) { const img = options.img; const clipInfo = options.clipInfo; From d9038b182441ef661489cd5e11cb05cc77fb69d9 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 14:19:56 +0100 Subject: [PATCH 19/27] LD681 - WIP, combined old and new approach for different img filling (half vs full) --- src/styles/graphicStrokeStyle.js | 759 +++++++++++++++++++++++++------ 1 file changed, 624 insertions(+), 135 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 2b36af22..e2317c80 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -15,6 +15,9 @@ import { splitLineString, getGapCloserPoints, getIsRightTurn, + getMirroredCoords, + angleInRadiansAtB, + calculatePointsDistance, } from './geometryCalcs'; // A flag to prevent multiple renderer patches. @@ -33,6 +36,7 @@ const clipInfoHashToBase64 = { leftTurn: new Map(), straight: new Map(), }; +const imageSourceHashToIsFullImg = new Map(); const USE_CACHING = true; const TURN_DIRECTION_CONFIG = { @@ -163,6 +167,12 @@ async function renderStrokeMarks( } const gapSize = graphicSpacing * pixelRatio; + const isFullImg = await getIsFullImg({ + image: ogImage, + imageWidth: ogImageWidth, + imageHeight: ogImageHeight, + pixelRatio, + }); const splitPoints = splitLineString( new LineString(pixelCoords), @@ -221,141 +231,30 @@ async function renderStrokeMarks( point.isFirstOfGeometry = isFirstOfGeometry; point.isRegularButShortened = isRegularButShortened; - let newPointsDataToRender; - if (isRightTurn) { - newPointsDataToRender = await handleRightTurn({ - currentGeometryCoordIndex: currentGeometryCoordIndex, - pointsDataToRender: pointsDataToRender, - pImage: ogImage, - pImageWidth: ogImageWidth, - pImageHeight: ogImageHeight, - pRenderContext: renderContext, - pPixelRatio: pixelRatio, - ogImageWidth: ogImageWidth, - ogImageHeight: ogImageHeight, - splitPoint: point, - gapSize: gapSize, - onlyDoGap: false, - involvedGeometryCoords: { - coordOnFirstLine: pixelCoords[currentGeometryCoordIndex - 1], - intersectCoord: pixelCoords[currentGeometryCoordIndex], - coordOnSecondLine: pixelCoords[currentGeometryCoordIndex + 1], - }, - }); - } - else if (isLeftTurn) { - newPointsDataToRender = await handleLeftTurn({ - currentGeometryCoordIndex: currentGeometryCoordIndex, - pointsDataToRender: pointsDataToRender, - pImage: ogImage, - pImageWidth: ogImageWidth, - pImageHeight: ogImageHeight, - pRenderContext: renderContext, - pPixelRatio: pixelRatio, - ogImageWidth: ogImageWidth, - ogImageHeight: ogImageHeight, - splitPoint: point, - gapSize, - onlyDoGap: false, - involvedGeometryCoords: { - coordOnFirstLine: pixelCoords[currentGeometryCoordIndex - 1], - intersectCoord: pixelCoords[currentGeometryCoordIndex], - coordOnSecondLine: pixelCoords[currentGeometryCoordIndex + 1], - }, - }); - } - else if (isRegularButShortened) { - newPointsDataToRender = await handleRegularButShortened({ - point, - gapSize, - ogImageWidth, - ogImageHeight, - ogImage, - pixelRatio, - image, - renderCoords, - customRender, - currentGeometryCoordIndex, - }); - } - else { - // Unchanged render - newPointsDataToRender = [ - { - // ignore: true, - image: image, - angle: point.angle, - coords: renderCoords, - rendererToUse: customRender, - geometryCoordIndex: currentGeometryCoordIndex, - }, - ]; - } - - // Polygon closing handling - const isPolygon = geometryType.includes('olygon'); - if (isPolygon) { - const isLastSplitPoint = i === splitPoints.length - 1; - if (isLastSplitPoint) { - const hasAdditionalPixelCoord = point.startingGeometryCoordIndex === pixelCoords.length - 2; - if (hasAdditionalPixelCoord) { - const nextPixelIsClosingPoint = point.startingGeometryCoordIndex !== 0 - && pixelCoords[point.startingGeometryCoordIndex + 1][0] === pixelCoords[0][0] - && pixelCoords[point.startingGeometryCoordIndex + 1][1] === pixelCoords[0][1]; - if (nextPixelIsClosingPoint) { - const lastSplitPoint = point; - const firstSplitPoint = splitPoints[0]; - const endOfPolygonIsRightTurn = getIsRightTurn( - pixelCoords[lastSplitPoint.startingGeometryCoordIndex], - pixelCoords[lastSplitPoint.startingGeometryCoordIndex + 1], // Is the same as [0] - pixelCoords[1], - ); - const endOfPolygonGapFillRenderData = endOfPolygonIsRightTurn - ? await handleRightTurn({ - currentGeometryCoordIndex: firstSplitPoint.startingGeometryCoordIndex, - pointsDataToRender: pointsDataToRender, - pImage: ogImage, - pImageWidth: ogImageWidth, - pImageHeight: ogImageHeight, - pRenderContext: renderContext, - pPixelRatio: pixelRatio, - ogImageWidth: ogImageWidth, - ogImageHeight: ogImageHeight, - splitPoint: firstSplitPoint, - gapSize: gapSize, - onlyDoGap: true, - involvedGeometryCoords: { - coordOnFirstLine: pixelCoords[lastSplitPoint.startingGeometryCoordIndex], - intersectCoord: pixelCoords[0], - coordOnSecondLine: pixelCoords[1], - }, - }) - : await handleLeftTurn({ - currentGeometryCoordIndex: firstSplitPoint.startingGeometryCoordIndex, - pointsDataToRender: pointsDataToRender, - pImage: ogImage, - pImageWidth: ogImageWidth, - pImageHeight: ogImageHeight, - pRenderContext: renderContext, - pPixelRatio: pixelRatio, - ogImageWidth: ogImageWidth, - ogImageHeight: ogImageHeight, - splitPoint: firstSplitPoint, - gapSize: gapSize, - onlyDoGap: true, - involvedGeometryCoords: { - coordOnFirstLine: pixelCoords[lastSplitPoint.startingGeometryCoordIndex], - intersectCoord: pixelCoords[0], - coordOnSecondLine: pixelCoords[1], - }, - }); - newPointsDataToRender.push(endOfPolygonGapFillRenderData[0]); - newPointsDataToRender.push(endOfPolygonGapFillRenderData[1]); - } - } - } - } - // end/ Polygon closing handling + 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; @@ -379,6 +278,291 @@ async function renderStrokeMarks( }); } +async function getNewPointsDataToRender(options) { + if (options.isFullImg) { + return getNewPointsDataToRenderForFullImg(options); + } + + return getNewPointsDataToRenderForHalfImg(options); +} + +async function getNewPointsDataToRenderForFullImg(options) { + const { + point, + image, + renderCoords, + customRender, + currentGeometryCoordIndex, + isRightTurn, + isLeftTurn, + isRegularButShortened, + } = options; + let newPointsDataToRender; + + if (isRightTurn) { + newPointsDataToRender = await handleRightTurn(getTurnHandlerOptions(options, { + splitPoint: point, + onlyDoGap: false, + involvedGeometryCoords: getInvolvedGeometryCoords(options.pixelCoords, currentGeometryCoordIndex), + })); + } else if (isLeftTurn) { + newPointsDataToRender = await handleLeftTurn(getTurnHandlerOptions(options, { + splitPoint: point, + onlyDoGap: false, + involvedGeometryCoords: getInvolvedGeometryCoords(options.pixelCoords, currentGeometryCoordIndex), + })); + } else if (isRegularButShortened) { + newPointsDataToRender = await handleRegularButShortened(getRegularButShortenedOptions(options)); + } else { + newPointsDataToRender = [createUnchangedPointData(options)]; + } + + const polygonClosingGapFillRenderData = await getPolygonClosingGapFillRenderDataForFullImg(options); + if (polygonClosingGapFillRenderData) { + newPointsDataToRender.push(...polygonClosingGapFillRenderData); + } + + return newPointsDataToRender; +} + +async function getNewPointsDataToRenderForHalfImg(options) { + const { + i, + point, + splitPoints, + isRightTurn, + isLeftTurn, + isFirstOfSegment, + isRegularButShortened, + } = options; + const isFirstAfterLeftTurn = !isLeftTurn + && !isRightTurn + && !isFirstOfSegment + && i > 1 + && splitPoints[i - 1].isLeftTurn; + + point.isFirstAfterLeftTurn = isFirstAfterLeftTurn; + + let newPointsDataToRender; + if (isRightTurn) { + newPointsDataToRender = await handleRightTurn(getTurnHandlerOptions(options, { + splitPoint: point, + onlyDoGap: false, + involvedGeometryCoords: getInvolvedGeometryCoords(options.pixelCoords, options.currentGeometryCoordIndex), + })); + } else if (isLeftTurn) { + newPointsDataToRender = await handleHalfImageLeftTurn(options); + } else if (isFirstAfterLeftTurn) { + newPointsDataToRender = await handleFirstAfterLeftTurn(options); + } else if (isRegularButShortened) { + newPointsDataToRender = await handleRegularButShortened(getRegularButShortenedOptions(options)); + } else { + newPointsDataToRender = [createUnchangedPointData(options)]; + } + + const polygonClosingGapFillRenderData = await getPolygonClosingGapFillRenderDataForHalfImg(options); + if (polygonClosingGapFillRenderData) { + newPointsDataToRender.push(...polygonClosingGapFillRenderData); + } + + return newPointsDataToRender; +} + +function getTurnHandlerOptions(options, overrides = {}) { + return { + currentGeometryCoordIndex: options.currentGeometryCoordIndex, + pointsDataToRender: options.pointsDataToRender, + pImage: options.ogImage, + pImageWidth: options.ogImageWidth, + pImageHeight: options.ogImageHeight, + pRenderContext: options.renderContext, + pPixelRatio: options.pixelRatio, + ogImageWidth: options.ogImageWidth, + ogImageHeight: options.ogImageHeight, + gapSize: options.gapSize, + ...overrides, + }; +} + +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, + }; +} + +function getInvolvedGeometryCoords(pixelCoords, geometryCoordIndex) { + return { + coordOnFirstLine: pixelCoords[geometryCoordIndex - 1], + intersectCoord: pixelCoords[geometryCoordIndex], + coordOnSecondLine: pixelCoords[geometryCoordIndex + 1], + }; +} + +function createUnchangedPointData(options) { + return { + image: options.image, + angle: options.point.angle, + coords: options.renderCoords, + rendererToUse: options.customRender, + geometryCoordIndex: options.currentGeometryCoordIndex, + }; +} + +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], + }, + }; +} + +async function getPolygonClosingGapFillRenderDataForFullImg(options) { + const polygonClosingContext = getPolygonClosingContext(options); + if (!polygonClosingContext) { + return null; + } + + const turnHandler = polygonClosingContext.endOfPolygonIsRightTurn + ? handleRightTurn + : handleLeftTurn; + return turnHandler(getTurnHandlerOptions(options, { + currentGeometryCoordIndex: polygonClosingContext.firstSplitPoint.startingGeometryCoordIndex, + splitPoint: polygonClosingContext.firstSplitPoint, + onlyDoGap: true, + involvedGeometryCoords: polygonClosingContext.involvedGeometryCoords, + })); +} + +async function getPolygonClosingGapFillRenderDataForHalfImg(options) { + const polygonClosingContext = getPolygonClosingContext(options); + if (!polygonClosingContext || !polygonClosingContext.endOfPolygonIsRightTurn) { + return null; + } + + return handleRightTurn(getTurnHandlerOptions(options, { + currentGeometryCoordIndex: polygonClosingContext.firstSplitPoint.startingGeometryCoordIndex, + splitPoint: polygonClosingContext.firstSplitPoint, + onlyDoGap: true, + involvedGeometryCoords: polygonClosingContext.involvedGeometryCoords, + })); +} + +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; +} + +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); + }); +} + function getTurnDirectionConfig(turnDirection) { const turnDirectionConfig = TURN_DIRECTION_CONFIG[turnDirection]; @@ -602,6 +786,207 @@ async function handleRegularButShortened(options) { return result; } +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, + }; + + const img = new Image(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + const clippedSrc = await getClippedImageForLeftTurn({ + img, + clipInfo, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + + const image = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + 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, + }; + const imgLeft = new Image(); + imgLeft.src = firstHalfOfLeftTurnRenderData.image.iconImage_.src_; + document.body.appendChild(imgLeft); + const firstHalfOfLeftTurnClippedSrc = await getClippedImageForLeftTurn({ + img: imgLeft, + clipInfo: adjustingClipInfo, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + const firstHalfOfLeftTurnImageAnchor = firstHalfOfLeftTurnRenderData.fromSplitPoint.isFirstOfGeometry + ? [0.5, 0.5] + : firstHalfOfLeftTurnRenderData.image.anchor_; + firstHalfOfLeftTurnRenderData.image = createOlIconWithDataURL({ + src: firstHalfOfLeftTurnClippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: firstHalfOfLeftTurnImageAnchor, + }); + + const hasRenderDataBeforeTurnOnSameSegment = pointsDataToRender.length - 2 >= 0 + && pointsDataToRender[pointsDataToRender.length - 2].geometryCoordIndex === firstHalfOfLeftTurnRenderData.geometryCoordIndex; + if (hasRenderDataBeforeTurnOnSameSegment) { + const lastRenderDataBeforeTurn = pointsDataToRender[pointsDataToRender.length - 2]; + const lastRenderDataBeforeTurnCutRatio = calculatePointsDistance( + lastRenderDataBeforeTurn.coords, + point.splitPointCoords, + ) / gapSize; + const lastRenderDataBeforeTurnCutLength = lastRenderDataBeforeTurnCutRatio * ogImageWidth; + const imgBeforeTurn = new Image(); + imgBeforeTurn.src = lastRenderDataBeforeTurn.image.iconImage_.src_; + document.body.appendChild(imgBeforeTurn); + const lastRenderDataBeforeTurnClippedSrc = await getClippedImageNoAngle({ + img: imgBeforeTurn, + clipInfo: { + cutLength: lastRenderDataBeforeTurnCutLength, + cutInFront: false, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + lastRenderDataBeforeTurn.image = createOlIconWithDataURL({ + src: lastRenderDataBeforeTurnClippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: lastRenderDataBeforeTurn.image.anchor_, + }); + } + } + + return [ + { + image, + angle: point.angle, + coords: point.splitPointCoords, + rendererToUse: toContext(renderContext), + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; +} + +async function handleFirstAfterLeftTurn(options) { + const { + i, + point, + splitPoints, + gapSize, + ogImageWidth, + ogImageHeight, + ogImage, + image, + renderCoords, + customRender, + currentGeometryCoordIndex, + } = options; + let clippedSrc; + + if (point.segmentLength === null || point.segmentLength === undefined) { + const distanceToPreviousSpPointInGapSize = calculatePointsDistance( + point.splitPointCoords, + splitPoints[i - 1].splitPointCoords, + ); + const distanceToPreviousSpPointRatio = distanceToPreviousSpPointInGapSize / gapSize; + const distanceToPreviousSpPoint = distanceToPreviousSpPointRatio * ogImageWidth; + const img = new Image(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + + if (distanceToPreviousSpPoint + 1e-11 < ogImageWidth) { + clippedSrc = await getClippedImageNoAngle({ + img, + clipInfo: { + cutLength: distanceToPreviousSpPoint, + cutInFront: true, + cutOnBothEnds: false, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } else { + clippedSrc = await getClippedImageNoAngle({ + img, + clipInfo: {}, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } + } else { + const img = new Image(); + img.src = ogImage.iconImage_.src_; + document.body.appendChild(img); + clippedSrc = await getClippedImageNoAngle({ + img, + clipInfo: { + cutLength: point.segmentLength, + cutInFront: false, + cutOnBothEnds: true, + }, + canvasWidth: ogImageWidth, + canvasHeight: ogImageHeight, + }); + } + + const clippedImage = createOlIconWithDataURL({ + src: clippedSrc, + imgSize: [ogImageWidth, ogImageHeight], + scale: ogImage.getScale(), + anchor: [0.5, 0.5], + }); + + return [ + { + image: clippedImage || image, + angle: point.angle, + coords: renderCoords, + rendererToUse: customRender, + geometryCoordIndex: currentGeometryCoordIndex, + }, + ]; +} + async function handleLeftTurn(options) { return handleTurn({ ...options, @@ -701,6 +1086,110 @@ function getClippedImageForTurn(options) { }); } +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); + }; + } + }); +} + function getClippedImageNoAngle(options) { const img = options.img; const clipInfo = options.clipInfo; From 354ff04912117df5f5e01d9422e47194ba9c3bb2 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 14:34:40 +0100 Subject: [PATCH 20/27] LD681 - WIP, refactoring --- src/styles/graphicStrokeStyle.js | 103 ++++++++++++++----------------- 1 file changed, 47 insertions(+), 56 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index e2317c80..6192f3af 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -288,39 +288,22 @@ async function getNewPointsDataToRender(options) { async function getNewPointsDataToRenderForFullImg(options) { const { - point, - image, - renderCoords, - customRender, - currentGeometryCoordIndex, isRightTurn, isLeftTurn, - isRegularButShortened, } = options; let newPointsDataToRender; if (isRightTurn) { - newPointsDataToRender = await handleRightTurn(getTurnHandlerOptions(options, { - splitPoint: point, - onlyDoGap: false, - involvedGeometryCoords: getInvolvedGeometryCoords(options.pixelCoords, currentGeometryCoordIndex), - })); + newPointsDataToRender = await handleCurrentTurn(options, handleRightTurn); } else if (isLeftTurn) { - newPointsDataToRender = await handleLeftTurn(getTurnHandlerOptions(options, { - splitPoint: point, - onlyDoGap: false, - involvedGeometryCoords: getInvolvedGeometryCoords(options.pixelCoords, currentGeometryCoordIndex), - })); - } else if (isRegularButShortened) { - newPointsDataToRender = await handleRegularButShortened(getRegularButShortenedOptions(options)); + newPointsDataToRender = await handleCurrentTurn(options, handleLeftTurn); } else { - newPointsDataToRender = [createUnchangedPointData(options)]; + newPointsDataToRender = await getRegularOrUnchangedPointsData(options); } - const polygonClosingGapFillRenderData = await getPolygonClosingGapFillRenderDataForFullImg(options); - if (polygonClosingGapFillRenderData) { - newPointsDataToRender.push(...polygonClosingGapFillRenderData); - } + await appendPolygonClosingGapFillRenderData(options, newPointsDataToRender, { + supportsClosingLeftTurn: true, + }); return newPointsDataToRender; } @@ -333,41 +316,52 @@ async function getNewPointsDataToRenderForHalfImg(options) { isRightTurn, isLeftTurn, isFirstOfSegment, - isRegularButShortened, } = options; - const isFirstAfterLeftTurn = !isLeftTurn + const isHalfImageFirstAfterLeftTurn = !isLeftTurn && !isRightTurn && !isFirstOfSegment && i > 1 && splitPoints[i - 1].isLeftTurn; - point.isFirstAfterLeftTurn = isFirstAfterLeftTurn; + point.isHalfImageFirstAfterLeftTurn = isHalfImageFirstAfterLeftTurn; let newPointsDataToRender; if (isRightTurn) { - newPointsDataToRender = await handleRightTurn(getTurnHandlerOptions(options, { - splitPoint: point, - onlyDoGap: false, - involvedGeometryCoords: getInvolvedGeometryCoords(options.pixelCoords, options.currentGeometryCoordIndex), - })); + newPointsDataToRender = await handleCurrentTurn(options, handleRightTurn); } else if (isLeftTurn) { newPointsDataToRender = await handleHalfImageLeftTurn(options); - } else if (isFirstAfterLeftTurn) { - newPointsDataToRender = await handleFirstAfterLeftTurn(options); - } else if (isRegularButShortened) { - newPointsDataToRender = await handleRegularButShortened(getRegularButShortenedOptions(options)); + } else if (isHalfImageFirstAfterLeftTurn) { + newPointsDataToRender = await handleHalfImageFirstAfterLeftTurn(options); } else { - newPointsDataToRender = [createUnchangedPointData(options)]; + newPointsDataToRender = await getRegularOrUnchangedPointsData(options); } - const polygonClosingGapFillRenderData = await getPolygonClosingGapFillRenderDataForHalfImg(options); - if (polygonClosingGapFillRenderData) { - newPointsDataToRender.push(...polygonClosingGapFillRenderData); - } + await appendPolygonClosingGapFillRenderData(options, newPointsDataToRender, { + supportsClosingLeftTurn: false, + }); return newPointsDataToRender; } +async function handleCurrentTurn(options, turnHandler) { + return turnHandler(getTurnHandlerOptions(options, { + splitPoint: options.point, + onlyDoGap: false, + involvedGeometryCoords: getInvolvedGeometryCoords( + options.pixelCoords, + options.currentGeometryCoordIndex, + ), + })); +} + +async function getRegularOrUnchangedPointsData(options) { + if (options.isRegularButShortened) { + return handleRegularButShortened(getRegularButShortenedOptions(options)); + } + + return [createUnchangedPointData(options)]; +} + function getTurnHandlerOptions(options, overrides = {}) { return { currentGeometryCoordIndex: options.currentGeometryCoordIndex, @@ -459,12 +453,23 @@ function getPolygonClosingContext(options) { }; } -async function getPolygonClosingGapFillRenderDataForFullImg(options) { +async function appendPolygonClosingGapFillRenderData(options, newPointsDataToRender, policy) { + const polygonClosingGapFillRenderData = await getPolygonClosingGapFillRenderData(options, policy); + if (polygonClosingGapFillRenderData) { + newPointsDataToRender.push(...polygonClosingGapFillRenderData); + } +} + +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; @@ -476,20 +481,6 @@ async function getPolygonClosingGapFillRenderDataForFullImg(options) { })); } -async function getPolygonClosingGapFillRenderDataForHalfImg(options) { - const polygonClosingContext = getPolygonClosingContext(options); - if (!polygonClosingContext || !polygonClosingContext.endOfPolygonIsRightTurn) { - return null; - } - - return handleRightTurn(getTurnHandlerOptions(options, { - currentGeometryCoordIndex: polygonClosingContext.firstSplitPoint.startingGeometryCoordIndex, - splitPoint: polygonClosingContext.firstSplitPoint, - onlyDoGap: true, - involvedGeometryCoords: polygonClosingContext.involvedGeometryCoords, - })); -} - async function getIsFullImg(options) { const { image, @@ -907,7 +898,7 @@ async function handleHalfImageLeftTurn(options) { ]; } -async function handleFirstAfterLeftTurn(options) { +async function handleHalfImageFirstAfterLeftTurn(options) { const { i, point, From 091a6257c96c3af72023ad29a6fbd9af0a484ae5 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 14:47:37 +0100 Subject: [PATCH 21/27] LD681 - some more code cleanup --- src/styles/graphicStrokeStyle.js | 285 +++++++++++-------------------- 1 file changed, 99 insertions(+), 186 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 6192f3af..3120c5e2 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -370,7 +370,6 @@ function getTurnHandlerOptions(options, overrides = {}) { pImageWidth: options.ogImageWidth, pImageHeight: options.ogImageHeight, pRenderContext: options.renderContext, - pPixelRatio: options.pixelRatio, ogImageWidth: options.ogImageWidth, ogImageHeight: options.ogImageHeight, gapSize: options.gapSize, @@ -599,43 +598,25 @@ async function handleTurn(options) { for (let i = 0; i < 2; i++) { const gapCloserPoint = gapCloserPoints[i]; - //let gapCloserImage = await deepCloneImage(pImage); - // 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; } - let gapCloserImage = new Image(); - gapCloserImage.src = pImage.iconImage_.src_; - document.body.appendChild(gapCloserImage); - - const clippedSrc = await getClippedImageForTurn({ - img: gapCloserImage, - clipInfo: gapCloserPoint, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - turnDirection, - }); const imageAnchor = gapCloserPoint.isFirst ? [0, 0.5] : [1, 0.5]; - gapCloserImage = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [pImageWidth, pImageHeight], - scale: pImage.getScale(), + const gapCloserImage = await clipToIcon({ + imageStyle: pImage, + clipper: getClippedImageForTurn, + clipInfo: gapCloserPoint, + canvasSize: [ogImageWidth, ogImageHeight], + clipperOptions: { + turnDirection, + }, anchor: imageAnchor, }); - // gapCloserImage = new Icon({ - // src: clippedSrc, - // imgSize: [pImageWidth, pImageHeight], - // scale: pImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // gapCloserImage.getImage(pPixelRatio).src = clippedSrc; const gapCloserRenderer = toContext(pRenderContext); @@ -663,34 +644,14 @@ async function handleTurn(options) { const nextSegmentClipInfo = { cutLength: nextSegmentCutLength, }; - //const clonedImage = await deepCloneImage(pImage); - const img = new Image(); - // img.src = clonedImage.getSrc(); - img.src = pImage.iconImage_.src_; - document.body.appendChild(img); - - const nextSegmentClippedSrc = await getClippedImageNoAngle({ - img: img, - clipInfo: nextSegmentClipInfo, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); const nextSegmentImageAnchor = [0, 0.5]; - const nextSegmentImage = createOlIconWithDataURL({ - src: nextSegmentClippedSrc, - imgSize: [pImageWidth, pImageHeight], - scale: pImage.getScale(), + const nextSegmentImage = await clipToIcon({ + imageStyle: pImage, + clipper: getClippedImageNoAngle, + clipInfo: nextSegmentClipInfo, + canvasSize: [ogImageWidth, ogImageHeight], anchor: nextSegmentImageAnchor, }); - // const nextSegmentImage = new Icon({ - // src: nextSegmentClippedSrc, - // imgSize: [pImageWidth, pImageHeight], - // scale: pImage.getScale(), - // anchor: nextSegmentImageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // nextSegmentImage.getImage(pPixelRatio).src = nextSegmentClippedSrc; const nextSegmentRenderer = toContext(pRenderContext); @@ -718,23 +679,20 @@ async function handleRegularButShortened(options) { const point = options.point; const gapSize = options.gapSize; const ogImageWidth = options.ogImageWidth; - const ogImageHeight = options.ogImageHeight; const ogImage = options.ogImage; - const pixelRatio = options.pixelRatio; let image = options.image; const renderCoords = options.renderCoords; const customRender = options.customRender; const currentGeometryCoordIndex = options.currentGeometryCoordIndex; - //image = await deepCloneImage(ogImage); - const img = new Image(); - // img.src = image.getSrc(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); const cutRatio = point.segmentLength / gapSize; const cutLength = cutRatio * ogImageWidth; - const clippedSrc = await getClippedImageNoAngle({ - img: img, + const imageAnchor = point.isFirstOfGeometry + ? [0.5, 0.5] + : [0, 0.5]; + image = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, clipInfo: { cutLength: point.isFirstOfGeometry ? ogImageWidth - cutLength @@ -742,27 +700,9 @@ async function handleRegularButShortened(options) { cutInFront: false, cutOnBothEnds: point.isFirstOfGeometry, }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - const imageAnchor = point.isFirstOfGeometry - ? [0.5, 0.5] - : [0, 0.5]; - image = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), + canvasSize: [ogImageWidth, options.ogImageHeight], anchor: imageAnchor, }); - // image = new Icon({ - // src: clippedSrc, - // imgSize: [ogImageWidth, ogImageHeight], - // scale: ogImage.getScale(), - // anchor: imageAnchor, - // anchorXUnits: 'fraction', - // anchorYUnits: 'fraction', - // }); - // image.getImage(pixelRatio).src = clippedSrc; const result = [ { @@ -811,20 +751,11 @@ async function handleHalfImageLeftTurn(options) { cutAngle, }; - const img = new Image(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - const clippedSrc = await getClippedImageForLeftTurn({ - img, + const image = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageForLeftTurn, clipInfo, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - - const image = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), + canvasSize: [ogImageWidth, ogImageHeight], anchor: [0.5, 0.5], }); @@ -838,23 +769,14 @@ async function handleHalfImageLeftTurn(options) { cutHeight: 0.5 * ogImageHeight, cutAngle, }; - const imgLeft = new Image(); - imgLeft.src = firstHalfOfLeftTurnRenderData.image.iconImage_.src_; - document.body.appendChild(imgLeft); - const firstHalfOfLeftTurnClippedSrc = await getClippedImageForLeftTurn({ - img: imgLeft, + firstHalfOfLeftTurnRenderData.image = await clipToIcon({ + imageStyle: firstHalfOfLeftTurnRenderData.image, + clipper: getClippedImageForLeftTurn, clipInfo: adjustingClipInfo, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - const firstHalfOfLeftTurnImageAnchor = firstHalfOfLeftTurnRenderData.fromSplitPoint.isFirstOfGeometry - ? [0.5, 0.5] - : firstHalfOfLeftTurnRenderData.image.anchor_; - firstHalfOfLeftTurnRenderData.image = createOlIconWithDataURL({ - src: firstHalfOfLeftTurnClippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: firstHalfOfLeftTurnImageAnchor, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: firstHalfOfLeftTurnRenderData.fromSplitPoint.isFirstOfGeometry + ? [0.5, 0.5] + : firstHalfOfLeftTurnRenderData.image.anchor_, }); const hasRenderDataBeforeTurnOnSameSegment = pointsDataToRender.length - 2 >= 0 @@ -866,22 +788,14 @@ async function handleHalfImageLeftTurn(options) { point.splitPointCoords, ) / gapSize; const lastRenderDataBeforeTurnCutLength = lastRenderDataBeforeTurnCutRatio * ogImageWidth; - const imgBeforeTurn = new Image(); - imgBeforeTurn.src = lastRenderDataBeforeTurn.image.iconImage_.src_; - document.body.appendChild(imgBeforeTurn); - const lastRenderDataBeforeTurnClippedSrc = await getClippedImageNoAngle({ - img: imgBeforeTurn, + lastRenderDataBeforeTurn.image = await clipToIcon({ + imageStyle: lastRenderDataBeforeTurn.image, + clipper: getClippedImageNoAngle, clipInfo: { cutLength: lastRenderDataBeforeTurnCutLength, cutInFront: false, }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, - }); - lastRenderDataBeforeTurn.image = createOlIconWithDataURL({ - src: lastRenderDataBeforeTurnClippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), + canvasSize: [ogImageWidth, ogImageHeight], anchor: lastRenderDataBeforeTurn.image.anchor_, }); } @@ -912,7 +826,7 @@ async function handleHalfImageFirstAfterLeftTurn(options) { customRender, currentGeometryCoordIndex, } = options; - let clippedSrc; + let clippedImage; if (point.segmentLength === null || point.segmentLength === undefined) { const distanceToPreviousSpPointInGapSize = calculatePointsDistance( @@ -921,52 +835,42 @@ async function handleHalfImageFirstAfterLeftTurn(options) { ); const distanceToPreviousSpPointRatio = distanceToPreviousSpPointInGapSize / gapSize; const distanceToPreviousSpPoint = distanceToPreviousSpPointRatio * ogImageWidth; - const img = new Image(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); if (distanceToPreviousSpPoint + 1e-11 < ogImageWidth) { - clippedSrc = await getClippedImageNoAngle({ - img, + clippedImage = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, clipInfo: { cutLength: distanceToPreviousSpPoint, cutInFront: true, cutOnBothEnds: false, }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], }); } else { - clippedSrc = await getClippedImageNoAngle({ - img, + clippedImage = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, clipInfo: {}, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], }); } } else { - const img = new Image(); - img.src = ogImage.iconImage_.src_; - document.body.appendChild(img); - clippedSrc = await getClippedImageNoAngle({ - img, + clippedImage = await clipToIcon({ + imageStyle: ogImage, + clipper: getClippedImageNoAngle, clipInfo: { cutLength: point.segmentLength, cutInFront: false, cutOnBothEnds: true, }, - canvasWidth: ogImageWidth, - canvasHeight: ogImageHeight, + canvasSize: [ogImageWidth, ogImageHeight], + anchor: [0.5, 0.5], }); } - const clippedImage = createOlIconWithDataURL({ - src: clippedSrc, - imgSize: [ogImageWidth, ogImageHeight], - scale: ogImage.getScale(), - anchor: [0.5, 0.5], - }); - return [ { image: clippedImage || image, @@ -1306,45 +1210,6 @@ function renderPoint(options) { renderToUse.drawPoint(pointToDraw); } -// Not quite a deep clone, but deep cloning the properties we need for rendering. -// We do this to not affect all individually rendered images when adjusting some of them. -async function deepCloneImage(image) { - - return new Promise((res, _) => { - if (image.getImage().complete) { - const copy = image.clone(); - - copy.imgSize_ = structuredClone(image.imgSize_); - copy.iconImage_ = new image.iconImage_.__proto__.constructor( - image.iconImage_.image_, - image.iconImage_.src_, - [image.iconImage_.size_[0], image.iconImage_.size_[1]], - image.iconImage_.crossOrigin_, - image.iconImage_.imageState_, - image.iconImage_.color_, - ); - - res(copy); - } else { - image.getImage().onload = () => { - const copy = image.clone(); - - copy.imgSize_ = structuredClone(image.imgSize_); - copy.iconImage_ = new image.iconImage_.__proto__.constructor( - image.iconImage_.image_, - image.iconImage_.src_, - [image.iconImage_.size_[0], image.iconImage_.size_[1]], - image.iconImage_.crossOrigin_, - image.iconImage_.imageState_, - image.iconImage_.color_, - ); - - res(copy); - }; - } - }); -} - function getHashCode(object) { if (!USE_CACHING) { return 1; @@ -1417,6 +1282,54 @@ function isCanvasEmpty(canvasSrc, canvasWidth, canvasHeight) { } +function createDomImage(src) { + const img = new Image(); + img.src = src; + document.body.appendChild(img); + return img; +} + +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, + }); +} + +function getImageStyleSrc(imageStyle) { + return imageStyle.iconImage_?.src_ || imageStyle.getSrc?.(); +} + +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 From 280a439924a578f103c38062d4d229ea394fd260 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Mon, 9 Mar 2026 16:04:33 +0100 Subject: [PATCH 22/27] LD681 - adjust clipping logic for segment before left turn --- src/styles/graphicStrokeStyle.js | 41 +++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 3120c5e2..ac9fc74a 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -783,21 +783,34 @@ async function handleHalfImageLeftTurn(options) { && pointsDataToRender[pointsDataToRender.length - 2].geometryCoordIndex === firstHalfOfLeftTurnRenderData.geometryCoordIndex; if (hasRenderDataBeforeTurnOnSameSegment) { const lastRenderDataBeforeTurn = pointsDataToRender[pointsDataToRender.length - 2]; - const lastRenderDataBeforeTurnCutRatio = calculatePointsDistance( + const incomingSpacing = 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_, - }); + 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_, + }); + } } } From 1a7f83a83263c1d6b842b52f376fab2fcc2ea5d4 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Tue, 10 Mar 2026 09:27:11 +0100 Subject: [PATCH 23/27] LD681 - minor renaming --- src/styles/graphicStrokeStyle.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index ac9fc74a..b7c0de57 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -317,20 +317,20 @@ async function getNewPointsDataToRenderForHalfImg(options) { isLeftTurn, isFirstOfSegment, } = options; - const isHalfImageFirstAfterLeftTurn = !isLeftTurn + const isFirstAfterLeftTurn = !isLeftTurn && !isRightTurn && !isFirstOfSegment && i > 1 && splitPoints[i - 1].isLeftTurn; - point.isHalfImageFirstAfterLeftTurn = isHalfImageFirstAfterLeftTurn; + point.isFirstAfterLeftTurn = isFirstAfterLeftTurn; let newPointsDataToRender; if (isRightTurn) { newPointsDataToRender = await handleCurrentTurn(options, handleRightTurn); } else if (isLeftTurn) { newPointsDataToRender = await handleHalfImageLeftTurn(options); - } else if (isHalfImageFirstAfterLeftTurn) { + } else if (isFirstAfterLeftTurn) { newPointsDataToRender = await handleHalfImageFirstAfterLeftTurn(options); } else { newPointsDataToRender = await getRegularOrUnchangedPointsData(options); From e4c0a6a03da9510ed522fb18d6e088ad7d73932d Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Tue, 10 Mar 2026 10:13:36 +0100 Subject: [PATCH 24/27] LD681 - add generated jdocs --- src/styles/graphicStrokeStyle.js | 177 +++++++++++++++++++++++++++++++ 1 file changed, 177 insertions(+) diff --git a/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index b7c0de57..64d29d6b 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -278,6 +278,11 @@ async function renderStrokeMarks( }); } +/** + * 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); @@ -286,6 +291,12 @@ async function getNewPointsDataToRender(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, @@ -308,6 +319,11 @@ async function getNewPointsDataToRenderForFullImg(options) { 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, @@ -343,6 +359,13 @@ async function getNewPointsDataToRenderForHalfImg(options) { 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, @@ -354,6 +377,11 @@ async function handleCurrentTurn(options, turnHandler) { })); } +/** + * 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)); @@ -362,6 +390,12 @@ async function getRegularOrUnchangedPointsData(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, @@ -377,6 +411,11 @@ function getTurnHandlerOptions(options, 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, @@ -392,6 +431,12 @@ function getRegularButShortenedOptions(options) { }; } +/** + * 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], @@ -400,6 +445,11 @@ function getInvolvedGeometryCoords(pixelCoords, geometryCoordIndex) { }; } +/** + * 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, @@ -410,6 +460,12 @@ function createUnchangedPointData(options) { }; } +/** + * 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, @@ -452,6 +508,14 @@ function getPolygonClosingContext(options) { }; } +/** + * 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) { @@ -459,6 +523,13 @@ async function appendPolygonClosingGapFillRenderData(options, newPointsDataToRen } } +/** + * 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) { @@ -480,6 +551,12 @@ async function getPolygonClosingGapFillRenderData(options, policy) { })); } +/** + * 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, @@ -530,6 +607,13 @@ async function getIsFullImg(options) { 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 => { @@ -553,6 +637,11 @@ function getLoadedImageElement(image, pixelRatio) { }); } +/** + * 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]; @@ -563,6 +652,11 @@ function getTurnDirectionConfig(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, @@ -570,6 +664,11 @@ async function handleRightTurn(options) { }); } +/** + * 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; @@ -675,6 +774,12 @@ async function handleTurn(options) { 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; @@ -717,6 +822,12 @@ async function handleRegularButShortened(options) { 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, @@ -825,6 +936,12 @@ async function handleHalfImageLeftTurn(options) { ]; } +/** + * 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, @@ -895,6 +1012,12 @@ async function handleHalfImageFirstAfterLeftTurn(options) { ]; } +/** + * 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, @@ -902,6 +1025,12 @@ async function handleLeftTurn(options) { }); } +/** + * 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; @@ -994,6 +1123,12 @@ function getClippedImageForTurn(options) { }); } +/** + * 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, @@ -1098,6 +1233,12 @@ function getClippedImageForLeftTurn(options) { }); } +/** + * 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; @@ -1223,6 +1364,12 @@ function renderPoint(options) { 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; @@ -1266,6 +1413,14 @@ function getHashCode(object) { 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'); @@ -1295,6 +1450,12 @@ function isCanvasEmpty(canvasSrc, canvasWidth, canvasHeight) { } +/** + * 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; @@ -1302,6 +1463,12 @@ function createDomImage(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, @@ -1332,10 +1499,20 @@ async function clipToIcon(options) { }); } +/** + * 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_ From 712e3846083ea6d284936430a929d270c42f0765 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Tue, 10 Mar 2026 10:48:04 +0100 Subject: [PATCH 25/27] v0.8.0. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50f042b8..3373699e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@disy/sldreader", - "version": "0.7.0", + "version": "0.8.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@disy/sldreader", - "version": "0.7.0", + "version": "0.8.0", "license": "ISC", "devDependencies": { "@babel/core": "^7.28.4", diff --git a/package.json b/package.json index b21fe2f6..64598f8d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.7.0", + "version": "0.8.0", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [ From af4f81c43f8191e911c3414a39e8f759ddf5a2c6 Mon Sep 17 00:00:00 2001 From: Sebastian Walter Date: Fri, 20 Mar 2026 13:10:50 +0100 Subject: [PATCH 26/27] LD683 - don't add the images to the dom; also upgrade ol to same as cm --- package-lock.json | 2 +- package.json | 2 +- src/styles/graphicStrokeStyle.js | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3373699e..1d55464e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,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/package.json b/package.json index 64598f8d..830a999e 100644 --- a/package.json +++ b/package.json @@ -57,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/src/styles/graphicStrokeStyle.js b/src/styles/graphicStrokeStyle.js index 64d29d6b..a460b30a 100644 --- a/src/styles/graphicStrokeStyle.js +++ b/src/styles/graphicStrokeStyle.js @@ -1459,7 +1459,6 @@ function isCanvasEmpty(canvasSrc, canvasWidth, canvasHeight) { function createDomImage(src) { const img = new Image(); img.src = src; - document.body.appendChild(img); return img; } From 5975c4c7c44808f7124c5b8849af7e74909470e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabian=20Ko=CC=88bel?= Date: Fri, 20 Mar 2026 13:19:31 +0100 Subject: [PATCH 27/27] v0.9.0. --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1d55464e..d6844c88 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@disy/sldreader", - "version": "0.8.0", + "version": "0.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@disy/sldreader", - "version": "0.8.0", + "version": "0.9.0", "license": "ISC", "devDependencies": { "@babel/core": "^7.28.4", diff --git a/package.json b/package.json index 830a999e..b6c7ca1e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@disy/sldreader", - "version": "0.8.0", + "version": "0.9.0", "description": "SLD reader and formatter for openlayers", "main": "dist/sldreader.js", "keywords": [