StorymapperArrow/MapArrow.js
2025-05-27 18:12:47 +02:00

395 lines
12 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.

import * as turf from "@turf/turf";
import { toMercator, toWgs84 } from '@turf/projection';
import { ARROW_BODY_STYLE_CONSTANT, ARROW_BODY_STYLE_LINEAR, ARROW_BODY_STYLE_EXPONENTIAL } from "./Arrow.js";
mapboxgl.accessToken = 'pk.eyJ1Ijoib3V0ZG9vcm1hcHBpbmdjb21wYW55IiwiYSI6ImNqYmh3cDdjYzNsMnozNGxsYzlvMmk2bTYifQ.QqcZ4LVoLWnXafXdjZxnZg';
const map = new mapboxgl.Map({
container: 'map',
center: [10, 50],
zoom: 5
});
map.on('load', () => {
const fullPolygon = getArrowPolygon(arrowData, style, arrowHeadData);
const circleGeoJSON = getCirclePolygon(circleCenter, circleRadius, circleDensity);
const circle = turf.circle([20, 80], 120000, { units: "meters", steps: 64 }); // Circle created directly with the Turf library
const rectangleGeoJSON = getRectanglePolygon([20, 80], 2200, 2200);
//ARROW
map.addSource("arrow-shape", { type: "geojson", data: fullPolygon });
map.addLayer({
"id": "arrow-shape",
"type": "fill",
"source": "arrow-shape",
"paint": {
"fill-color": "#ff0000",
"fill-opacity": 0.7
}
});
map.addLayer({
"id": "arrow-outline",
"type": "line",
"source": "arrow-shape",
"paint": {
"line-color": "#000000",
"line-width": 2,
"line-opacity": 1
}
});
//ARROW
// CIRCLE
map.addSource("circlePolygon", {
"type": "geojson",
"data": circleGeoJSON
});
map.addLayer({
"id": "circlePolygon",
"type": "fill",
"source": "circlePolygon",
"layout": {},
"paint": {
"fill-color": "blue",
"fill-opacity": 0.6
}
});
map.addLayer({
"id": "circlePolygon-outline",
"type": "line",
"source": "circlePolygon",
"paint": {
"line-color": "#000000",
"line-width": 2,
"line-opacity": 1
}
});
// CIRCLE
// RECTANGLE
map.addSource("rectanglePolygon", {
"type": "geojson",
"data": rectangleGeoJSON
});
// Přidání vrstvy pro vykreslení polygonu
map.addLayer({
"id": "rectanglePolygon",
"type": "fill",
"source": "rectanglePolygon",
"layout": {},
"paint": {
"fill-color": "red",
"fill-opacity": 0.6
}
});
// Přidání outline pro polygon
map.addLayer({
"id": "rectanglePolygon-outline",
"type": "line",
"source": "rectanglePolygon",
"paint": {
"line-color": "#000",
"line-width": 3
}
});
// RECTANGLE
//MAP GRID
const grid = generateLatLonGrid(10);
map.addSource("latLonGrid", { type: "geojson", data: grid });
map.addLayer({
id: "latLonGrid",
type: "line",
source: "latLonGrid",
paint: {
"line-color": "#888",
"line-width": 1,
"line-opacity": 0.5
}
});
//MAP GRID
});
//ARROW
const points = [
[1.42076, 40.08804],
[15.42076, 80.08804],
[55.42076, 75.08804],
[120.42076, 40.08804],
[358.4050, 50.52]
];
const arrowData = {
points: points,
splineStep: 20,
spacing: 0.01,
offsetDistance: 20000
};
const style = {
calculation: ARROW_BODY_STYLE_CONSTANT,
range: 1,
minValue: 0.1
};
const arrowHeadData = {
widthArrow: 10,
lengthArrow: 5
};
//ARROW
//CIRCLE
const circleCenter = [20, 80];
const circleRadius = 120;
const circleDensity = 20;
//CORCLE
//ARROW
// Cubic interpolation source from https://www.paulinternet.nl/?page=bicubic
function cubicInterpolate(p, x) {
return p[1] + 0.5 * x * (p[2] - p[0] + x * (2.0 * p[0] - 5.0 * p[1] + 4.0 * p[2] - p[3] + x * (3.0 * (p[1] - p[2]) + p[3] - p[0])));
}
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 ;
}
/**
* @param {Object} arrowData - Object with data for arrow
* @param {Array<[number, 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} 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, arrowHeadData) {
if (!style)
style = {
calculation: ARROW_BODY_STYLE_CONSTANT,
range: 0,
minValue: 0
};
const splinePoints = computeSplinePoints(arrowData.points, arrowData.splineStep);
const { leftSidePoints, rightSidePoints } = computeSideOffsets(splinePoints, arrowData.offsetDistance, style);
const end = splinePoints[splinePoints.length -1];
const bearing = averageBearing(splinePoints, 3);
const arrowHead= arrowHeadData
? createIsoscelesTriangleCoords(
turf.point(end),
arrowData.offsetDistance * arrowHeadData.widthArrow, arrowData.offsetDistance * arrowHeadData.lengthArrow, bearing)
: [];
const polygonCoords = [
...leftSidePoints,
...arrowHead,
...rightSidePoints.reverse(),
leftSidePoints[0]
];
return turf.polygon([[...polygonCoords]]);
}
function averageBearing(points, count = 3) {
const bearings = [];
for (let i = points.length - count; i < points.length -1; i++) {
if (i >= 0) {
const b = turf.bearing(turf.point(points[i]), turf.point(points[i + 1]));
bearings.push(b);
}
}
const sinSum = bearings.reduce((sum, b) => sum + Math.sin(b * Math.PI / 180), 0);
const cosSum = bearings.reduce((sum, b) => sum + Math.cos(b * Math.PI / 180), 0);
return Math.atan2(sinSum, cosSum) * 180 / Math.PI;
}
function computeSplinePoints(points, splineStep = 10) {
if (points.length < 2) return points;
const result = [];
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 j = 0; j < splineStep; j++) {
const t = j / splineStep;
const lon = cubicInterpolate([p0[0], p1[0], p2[0], p3[0]], t);
const lat = cubicInterpolate([p0[1], p1[1], p2[1], p3[1]], t);
result.push([lon, lat]);
}
}
result.push(points[points.length - 1]);
return result;
}
function computeSideOffsets(points, offsetMeters, style) {
let leftSidePoints = [];
let rightSidePoints = [];
const total = points.length - 1;
for (let i = 1; i < points.length; i++) {
const previousPoint = points[i - 1];
const currentPoint = points[i];
const bearing = turf.bearing(turf.point(previousPoint), turf.point(currentPoint));
const normalizedPosition = i / total;
let localOffsetDistance;
switch (style.calculation) {
case ARROW_BODY_STYLE_LINEAR:
localOffsetDistance = offsetMeters * linearWidthCurve(normalizedPosition, style.range, style.minValue);
break;
case ARROW_BODY_STYLE_EXPONENTIAL:
localOffsetDistance = offsetMeters * exponentialWidthCurve(normalizedPosition, style.range, style.minValue);
break;
case ARROW_BODY_STYLE_CONSTANT:
default:
localOffsetDistance = offsetMeters;
}
leftSidePoints.push(turf.destination(turf.point(currentPoint), localOffsetDistance, bearing - 90, { units: 'meters' }).geometry.coordinates);
rightSidePoints.push(turf.destination(turf.point(currentPoint), localOffsetDistance, bearing + 90, { units: 'meters' }).geometry.coordinates);
}
return { leftSidePoints, rightSidePoints };
}
function createIsoscelesTriangleCoords(center, baseLengthMeters, heightMeters, bearing = 0) {
const halfBase = baseLengthMeters / 2;
const left = turf.destination(center, halfBase, bearing - 90, { units: 'meters' }).geometry.coordinates;
const right = turf.destination(center, halfBase, bearing + 90, { units: 'meters' }).geometry.coordinates;
const tip = turf.destination(center, heightMeters, bearing, { units: 'meters' }).geometry.coordinates;
return [left, tip, right];
}
//ARROW
//CIRCLE
const distancePerDegreeLongitude = 111.320; // 2π×6378.1km/360
const distancePerDegreeLatitude = 110.574; // 2π×6356.75km/360
/**
* @param {Object} center - The center point of the circle.
* @param {number} center.x
* @param {number} center.y
* @param {number} radius - The radius of the circle.
* @param {number} density - The number of points used to approximate the circle.
*
* @returns {Object} GeoJSON Feature representing the circle polygon.
*/
export function getCirclePolygon(center, radius, density = 64) {
const points = [];
const coords = {
latitude: center[1], // Latitude
longitude: center[0] // Longitude
};
const distanceX = radius / (distancePerDegreeLongitude * Math.cos(coords.latitude * Math.PI / 180));
const distanceY = radius / distancePerDegreeLatitude;
for (let i = 0; i < density; i++) {
const angle = (i / density) * Math.PI * 2;
const x = distanceX * Math.cos(angle);
const y = distanceY * Math.sin(angle);
points.push([coords.longitude + x, coords.latitude + y]);
}
// Close the circle by adding the first point again
points.push(points[0]);
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [points]
},
properties: {}
};
}
//CIRCLE
//RECTANGLE
/**
* @param {Object} center - The center point of the rectangle.
* @param {number} center.x
* @param {number} center.y
* @param {number} width - The length of the first side of the rectangle.
* @param {number} height - The length of the second side of the rectangle.
* @param {number} rotation - The angle (in radians) by which to rotate the rectangle.
*
* @returns {Object} GeoJSON Feature representing the rectangle polygon.
*/
export function getRectanglePolygon(center, width, height, rotation = 0) {
const widthMeters = width * 1000 ;
const heightMeters = height * 1000;
const centerMerc = toMercator(turf.point(center)).geometry.coordinates;
const halfWidth = widthMeters / 2;
const halfHeight = heightMeters / 2;
let corners = [
[centerMerc[0] - halfWidth, centerMerc[1] + halfHeight], // topLeft
[centerMerc[0] + halfWidth, centerMerc[1] + halfHeight], // topRight
[centerMerc[0] + halfWidth, centerMerc[1] - halfHeight], // bottomRight
[centerMerc[0] - halfWidth, centerMerc[1] - halfHeight], // bottomLeft
];
if (rotation !== 0) {
const rad = (rotation * Math.PI) / 180;
corners = corners.map(([x, y]) => rotateXY(x, y, centerMerc[0], centerMerc[1], rad));
}
corners.push(corners[0]);
const wgsCoords = corners.map(([x, y]) => toWgs84([x, y]));
return turf.polygon([wgsCoords]);
}
//RECTANGLE
//MAP GRID
function generateLatLonGrid(step = 10) {
const features = [];
for (let lat = -80; lat <= 80; lat += step) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: Array.from({ length: 37 }, (_, i) => [-180 + i * 10, lat])
}
});
}
for (let lon = -180; lon <= 180; lon += step) {
features.push({
type: "Feature",
geometry: {
type: "LineString",
coordinates: Array.from({ length: 17 }, (_, i) => [lon, -80 + i * 10])
}
});
}
return {
type: "FeatureCollection",
features
};
}
//MAP GRID