export const ARROW_BODY_STYLE_CONSTANT = 1; export const ARROW_BODY_STYLE_LINEAR = 2; export const ARROW_BODY_STYLE_EXPONENTIAL = 3; const distancePerDegreeLongitude = 111.320; // 2π×6378.1km/360 const distancePerDegreeLatitude = 110.574; // 2π×6356.75km/360 import * as turf from "@turf/turf"; function round(value, decimals = 6) { return Number(value.toFixed(decimals)); } // Převod z lon/lat do Web Mercator (v metrech) function lonLatToMeters(lon, lat) { const originShift = 2 * Math.PI * 6378137 / 2.0; const mx = lon * originShift / 180.0 ; const my = Math.log(Math.tan((90 + lat) * Math.PI / 360.0)) / (Math.PI / 180.0)* 150; //console.log(JSON.stringify(mx, null, 2)); return { x: mx, y: my * originShift / 180.0 }; } // Převod z Web Mercator zpět do lon/lat function metersToLonLat(mx, my) { const originShift = 2 * Math.PI * 6378137 / 2.0; const lon = (mx / originShift) * 180.0; const lat = 180 / Math.PI * (2 * Math.atan(Math.exp(my / originShift * Math.PI / 180.0)) - Math.PI / 2); return { lon: round(lon, 6), lat: round(lat, 6) }; } /** * @param {Object} arrowData - Object with data for arrow * @param {{x: number, y: number}[]} arrowData.points - List of points defining the arrow's path. * @param {number} arrowData.splineStep - The step size for the spline interpolation. * @param {number} arrowData.spacing - The spacing between the points along the arrow. * @param {number} arrowData.offsetDistance - The offset distance for the arrow's path (width). * * @param {Object} style - Object with data for the calculation style. * @param {number} style.calculation - The style for the calculation * @param {number} style.range - The range for the calculation style. * @param {number} style.minValue - The minimum value used in the calculation. * * @param {Object|undefined} arrowHeadData - Optional data for the arrowhead. * @param {number} arrowHeadData.widthArrow - The width of the arrowhead. * @param {number} arrowHeadData.lengthArrow - The length of the arrowhead. * * @returns {{x: number, y: number}[]} - An array of points representing the arrow polygon. */ export function getArrowPolygon( arrowData, style= undefined, arrowHeadData = undefined) { if (!style) style = { calculation: ARROW_BODY_STYLE_CONSTANT, range: 0, minValue: 0 }; const splinePoints = computeSplinePoints(arrowData.points, arrowData.splineStep); const { leftSidePoints, rightSidePoints } = computeSides(splinePoints, arrowData.spacing, arrowData.offsetDistance, style); const arrowHead= arrowHeadData ? createIsoscelesTriangleFromSpline(splinePoints, arrowHeadData.widthArrow, arrowHeadData.lengthArrow) : []; const fullPolygon = [...leftSidePoints, ...arrowHead.reverse(), ...rightSidePoints.reverse()]; return fullPolygon; } function computeSplinePoints(points, splineStep) { let 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 += splineStep) { splinePoints.push({ x: cubicInterpolate([p0.x, p1.x, p2.x, p3.x], t), y: cubicInterpolate([p0.y, p1.y, p2.y, p3.y], t) }); } } splinePoints.x /100; return splinePoints; } function computeSides(splinePoints, spacing, offsetDistance, style) { 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 segmentLength = Math.hypot(currentPoint.x - previousPoint.x, currentPoint.y - previousPoint.y); accumulatedDistance += segmentLength; if (accumulatedDistance >= spacing || i === 1 || i === splinePoints.length - 1) { let distanceX; let distanceY; if (i == 1 || i == splinePoints.length-1) { distanceX = (currentPoint.y - previousPoint.y); distanceY = (previousPoint.x - currentPoint.x); }else { distanceX = (currentPoint.y - previousPoint.y) ; distanceY = (previousPoint.x - currentPoint.x) ; } const length = Math.hypot(distanceX, distanceY); const normalizedPosition = i / (splinePoints.length - 1); let localOffsetDistance; switch (style.calculation) { case ARROW_BODY_STYLE_LINEAR: localOffsetDistance = offsetDistance * linearWidthCurve(normalizedPosition, style.range, style.minValue); break; case ARROW_BODY_STYLE_EXPONENTIAL: localOffsetDistance = offsetDistance * exponentialWidthCurve(normalizedPosition, style.range, style.minValue); break; case ARROW_BODY_STYLE_CONSTANT: default: localOffsetDistance = offsetDistance; } const offsetX = (distanceX / length) * localOffsetDistance; const offsetY = (distanceY / length) * localOffsetDistance; accumulatedDistance = 0; leftSidePoints.push({ x: currentPoint.x + offsetX , y: currentPoint.y + offsetY }); rightSidePoints.push({ x: currentPoint.x - offsetX , y: currentPoint.y - offsetY }); } } return { leftSidePoints, rightSidePoints }; } function createIsoscelesTriangleFromSpline(splinePoints, baseLengthMeters, heightMeters) { if (splinePoints.length < 2) { throw new Error("Potřeba alespoň dva body ve splinePoints"); } const last = splinePoints[splinePoints.length - 1]; const prev = splinePoints[splinePoints.length - 2]; // Vektor směru (bearing) const dx = last.x - prev.x; const dy = last.y - prev.y; const bearing = (Math.atan2(dx, dy) * 180 / Math.PI + 360) % 360; const center = [last.x, last.y]; // GeoJSON formát: [lon, lat] const halfBase = baseLengthMeters / 2*100000; const leftBase = turf.destination(center, halfBase, bearing - 90, { units: 'meters' }); const rightBase = turf.destination(center, halfBase, bearing + 90, { units: 'meters' }); const apex = turf.destination(center, heightMeters*100000, bearing, { units: 'meters' }); return [ { x: leftBase.geometry.coordinates[0], y: leftBase.geometry.coordinates[1] }, { x: apex.geometry.coordinates[0], y: apex.geometry.coordinates[1] }, { x: rightBase.geometry.coordinates[0], y: rightBase.geometry.coordinates[1] } ]; } function computeArrowHead(splinePoints, width, length) { const len = splinePoints.length; const lastPoint = splinePoints[len - 1]; const secondLastPoint = splinePoints[len - 2]; //const x = (lastPoint.x - secondLastPoint.x); //const y = (lastPoint.y - secondLastPoint.y); const x = (lastPoint.x - secondLastPoint.x) ; const y = (lastPoint.y - secondLastPoint.y) ; const magnitude = Math.hypot(x, y); const normalizedX = x / magnitude; const normalizedY = y / magnitude; return [ { x: lastPoint.x - normalizedY * width , y: lastPoint.y + normalizedX * width}, { x: (lastPoint.x + normalizedX) * length, y: (lastPoint.y + normalizedY) * length}, { x: lastPoint.x + normalizedY * width , y: lastPoint.y - normalizedX * width}, ]; } function exponentialWidthCurve(normalizedPosition, range = 5, minValue = 0.1) { return minValue + (1 - minValue) * Math.exp(-range * normalizedPosition); } function linearWidthCurve(normalizedPosition, range = 1, minValue = 0.1) { return 1 + (minValue - 1) * normalizedPosition / range ; }