import { cubicInterpolate } from "athena-utils/shape/Arrow.js"; import * as turf from '@turf/turf'; export const LEFT_SIDE = 1; export const RIGHT_SIDE = 2; export const BOTH_SIDES = 3; export const METERS = 'meters'; /** * @param {Object, Object} frontlineData, protrusionData - Object containing parameters for the frontline. * @param {{x: number, y: number}[]} frontlineData.points - List of points defining the base path of the frontline. * @param {number} frontlineData.splineStep - The resolution of the spline interpolation (smaller = smoother curve). * @param {number} frontlineData.spacing - Distance between interpolated points along the path. * @param {number} frontlineData.offsetDistance - Distance to offset the entire shape from the base path. * @param {string} frontlineData.style - Which side to draw the protrusions on (e.g., "LEFT_SIDE" or "RIGHT_SIDE"). * @param {number} protrusionData.Length - Length of each individual protrusion element. * @param {number} protrusionData.StartSize - Width of protrusion at the start (base). * @param {number} protrusionData.EndSize - Width of protrusion at the end (tip). * @param {number} protrusionData.Gap - Distance between the starts of each protrusion. * */ export function getFrontline(frontlineData, protrusionData = null) { if (!frontlineData || !(frontlineData.points) || frontlineData.points.length === 0) { console.warn("getFrontline: Invalid frontlineData or empty points array."); return []; } const style = frontlineData.style ?? LEFT_SIDE; const splinePoints = computeSplinePoints(frontlineData.points, frontlineData.splineStep); let bodyPolygonLeft = []; let bodyPolygonRight = []; if (style === BOTH_SIDES) { const { leftSidePoints: leftSidePointsLeftSide, rightSidePoints: rightSidePointsLeftSide } = computeSides(splinePoints, frontlineData.offsetDistance, LEFT_SIDE); bodyPolygonLeft = [...leftSidePointsLeftSide, ...rightSidePointsLeftSide.reverse()]; const { leftSidePoints: leftSidePointsRightSide, rightSidePoints: rightSidePointsRightSide } = computeSides(splinePoints, frontlineData.offsetDistance, RIGHT_SIDE); bodyPolygonRight = [...leftSidePointsRightSide, ...rightSidePointsRightSide.reverse()]; } const { leftSidePoints, rightSidePoints } = computeSides(splinePoints, frontlineData.offsetDistance, frontlineData.style); const bodyPolygon = [...leftSidePoints, ...rightSidePoints.reverse()]; if (protrusionData == null) { if (style === BOTH_SIDES) { const polygonCoords = [ ...bodyPolygonLeft, ...bodyPolygonRight, bodyPolygonLeft[0] ]; return turf.polygon([[...polygonCoords]]); } else { const polygonCoords = [ ...bodyPolygon, bodyPolygon[0] ]; return turf.polygon([[...polygonCoords]]); } } const polygonCoordsLeft= [ ...bodyPolygonLeft, bodyPolygonLeft[0] ]; const polygonCoordsRight= [ ...bodyPolygonRight, bodyPolygonRight[0] ]; const polygonCoords = [ ...bodyPolygon, bodyPolygon[0] ]; if (style === LEFT_SIDE) { return { body: turf.polygon([[...polygonCoords]]), protrusions: computeProtrusion(leftSidePoints, protrusionData), }; } else if (style === RIGHT_SIDE) { return { body: turf.polygon([[...polygonCoords]]), protrusions: computeProtrusion(rightSidePoints, protrusionData) }; } else if (style === BOTH_SIDES) { return { bodyLeft: turf.polygon([[...polygonCoordsLeft]]), bodyRight: turf.polygon([[...polygonCoordsRight]]), protrusionsLeft: computeProtrusion(leftSidePoints, protrusionData), protrusionsRight: computeProtrusion(rightSidePoints, protrusionData) }; } } function computeSplinePoints(points, density) { if (points.length < 2) return points; const splinePoints = []; for (let i = 0; i < points.length - 1; i++) { const p0 = points[i === 0 ? i : i - 1]; const p1 = points[i]; const p2 = points[i + 1]; const p3 = points[i + 2] || p2; for (let t = 0; t <= 1; t += density) { const lon = cubicInterpolate([p0[0], p1[0], p2[0], p3[0]], t); const lat = cubicInterpolate([p0[1], p1[1], p2[1], p3[1]], t); splinePoints.push([lon, lat]); } } splinePoints.push(points[points.length - 1]); return splinePoints; } function computeSides(splinePoints, offsetDistance, style = LEFT_SIDE) { let leftSidePoints = []; let rightSidePoints = []; let accumulatedDistance = 0; for (let i = 1; i < splinePoints.length; i++) { const previousPoint = splinePoints[i - 1]; const currentPoint = splinePoints[i]; const segmentDistance = turf.distance(turf.point(previousPoint), turf.point(currentPoint), { units: METERS }); accumulatedDistance += segmentDistance; const bearing = turf.bearing(turf.point(previousPoint), turf.point(currentPoint)); let leftPoint, rightPoint; if (style === LEFT_SIDE) { leftPoint = turf.destination(turf.point(currentPoint), offsetDistance, bearing - 90, { units: METERS }).geometry.coordinates; rightPoint = currentPoint; } else if (style === RIGHT_SIDE) { leftPoint = currentPoint; rightPoint = turf.destination(turf.point(currentPoint), offsetDistance, bearing + 90, { units: METERS }).geometry.coordinates; } else if (style === BOTH_SIDES) { leftPoint = turf.destination(turf.point(currentPoint), offsetDistance, bearing - 90, { units: METERS }).geometry.coordinates; rightPoint = turf.destination(turf.point(currentPoint), offsetDistance, bearing + 90, { units: METERS }).geometry.coordinates; } leftSidePoints.push(leftPoint); rightSidePoints.push(rightPoint); accumulatedDistance = 0; } return { leftSidePoints, rightSidePoints }; } function computeProtrusion(leftSidePoints, protrusionData) { const protrusions = []; const segments = []; let totalLength = 0; for (let i = 0; i < leftSidePoints.length - 1; i++) { const p0 = leftSidePoints[i]; const p1 = leftSidePoints[i + 1]; const length = turf.distance(turf.point(p0), turf.point(p1), { units: METERS }); const bearing = turf.bearing(turf.point(p0), turf.point(p1)); segments.push({ p0, p1, length, bearing }); totalLength += length; } const positions = []; for (let d = 0; d <= totalLength - (protrusionData.gap + protrusionData.startSize); d += protrusionData.gap) { positions.push(d + protrusionData.startSize); } if (positions[positions.length - 1] < totalLength) { positions.push(totalLength - protrusionData.startSize); } let currentSegmentIndex = 0; let currentSegmentPos = 0; for (const distance of positions) { while (currentSegmentIndex < segments.length && currentSegmentPos + segments[currentSegmentIndex].length < distance) { currentSegmentPos += segments[currentSegmentIndex].length; currentSegmentIndex++; } if (currentSegmentIndex >= segments.length) { break; } const seg = segments[currentSegmentIndex]; const localDistance = distance - currentSegmentPos; const pointOnSegment = turf.along(turf.lineString([seg.p0, seg.p1]), localDistance, { units: METERS }).geometry.coordinates; const normalBearing = seg.bearing - 90; const tangentBearing = seg.bearing; const centerPoint = turf.destination(turf.point(pointOnSegment), protrusionData.length, normalBearing, { units: METERS }).geometry.coordinates; const corner1 = turf.destination(turf.point(pointOnSegment), -protrusionData.startSize, tangentBearing, { units: METERS }).geometry.coordinates; const corner2 = turf.destination(turf.point(pointOnSegment), protrusionData.startSize, tangentBearing, { units: METERS }).geometry.coordinates; const corner3 = turf.destination(turf.point(centerPoint), protrusionData.endSize, tangentBearing, { units: METERS }).geometry.coordinates; const corner4 = turf.destination(turf.point(centerPoint), -protrusionData.endSize, tangentBearing, { units: METERS }).geometry.coordinates; const polygonCoords = [corner1, corner2, corner3, corner4, corner1]; const polygon = turf.polygon([polygonCoords]); protrusions.push(polygon); } return protrusions; }