StorymapperArrow/Arrow.js
2025-05-27 16:01:42 +02:00

210 lines
7.8 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ;
}