diff --git a/Arrow.js b/Arrow.js new file mode 100644 index 0000000..f9a0d3f --- /dev/null +++ b/Arrow.js @@ -0,0 +1,209 @@ +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 ; +} diff --git a/ArrowPoints.js b/ArrowPoints.js index a291333..3694fd6 100644 --- a/ArrowPoints.js +++ b/ArrowPoints.js @@ -1,16 +1,16 @@ const canvas = document.getElementById("canvas"); // Compute arrow polygon -import { getArrowPolygon } from "athena-utils/shape/Arrow.js"; -import { ARROW_BODY_STYLE_CONSTANT, ARROW_BODY_STYLE_LINEAR, ARROW_BODY_STYLE_EXPONENTIAL } from "athena-utils/shape/Arrow.js"; -import { getCirclePolygon } from "athena-utils/shape/BasicShapes.js"; -import { getRectanglePolygon } from "athena-utils/shape/BasicShapes.js"; -import { getFrontline } from "athena-utils/shape/Frontline.js"; -import { LEFT_SIDE, RIGHT_SIDE, BOTH_SIDES } from "athena-utils/shape/Frontline.js"; +import { getArrowPolygon } from "./Arrow.js"; +import { ARROW_BODY_STYLE_CONSTANT, ARROW_BODY_STYLE_LINEAR, ARROW_BODY_STYLE_EXPONENTIAL } from "./Arrow.js"; +import { getCirclePolygon } from "./BasicShapes.js"; +import { getRectanglePolygon } from "./BasicShapes.js"; +import { getFrontline } from "./Frontline.js"; +import { LEFT_SIDE, RIGHT_SIDE, BOTH_SIDES } from "./Frontline.js"; // Polygon merge using Turf library -import {mergeTurfPolygons} from "athena-utils/shape/Polygon.js"; -import {addTurfPolygonToMerge} from "athena-utils/shape/Polygon.js"; -import {toTurfPolygon} from "athena-utils/shape/Polygon.js"; -import {drawPolygon} from "athena-utils/shape/PolygonVisuals.js"; +import {mergeTurfPolygons} from "./Polygon.js"; +import {addTurfPolygonToMerge} from "./Polygon.js"; +import {toTurfPolygon} from "./Polygon.js"; +import {drawPolygon} from "./PolygonVisuals.js"; const circleCenter = {x:320, y:180}; const circleRadius = 70; diff --git a/BasicShapes.js b/BasicShapes.js new file mode 100644 index 0000000..053f814 --- /dev/null +++ b/BasicShapes.js @@ -0,0 +1,137 @@ +const distancePerDegreeLongitude = 111.320; // 2π×6378.1km/360 +const distancePerDegreeLatitude = 110.574; // 2π×6356.75km/360 + +import * as turf from "@turf/turf"; +import { toMercator, toWgs84 } from '@turf/projection'; + +/** + * @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: {} + }; +} + +function getDistancePerDegreeLongitude(latitude) { + return 111.320 * Math.cos(latitude * Math.PI / 180); +} + +/** + * @param {Object} center - The center point of the rectangle. + * @param {number} center.x + * @param {number} center.y + * @param {number} sideA - The length of the first side of the rectangle. + * @param {number} sideB - 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; + + // 1. Střed převedeme do metrického systému (Web Mercator) + const centerMerc = toMercator(turf.point(center)).geometry.coordinates; + + // 2. Vypočítáme rohy čtverce v metrech + 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 + ]; + + // 3. Otočení (volitelně) + if (rotation !== 0) { + const rad = (rotation * Math.PI) / 180; + corners = corners.map(([x, y]) => rotateXY(x, y, centerMerc[0], centerMerc[1], rad)); + } + + // 4. Uzavřeme polygon a převedeme zpět do WGS84 + corners.push(corners[0]); + const wgsCoords = corners.map(([x, y]) => toWgs84([x, y])); + + return turf.polygon([wgsCoords]); +} + +function rotateXY(x, y, cx, cy, angleRad) { + const dx = x - cx; + const dy = y - cy; + const cos = Math.cos(angleRad); + const sin = Math.sin(angleRad); + const rx = cx + dx * cos - dy * sin; + const ry = cy + dx * sin + dy * cos; + return [rx, ry]; +} + /* + const [lon, lat] = center; + + // Přepočet metrů na stupně: + const degLat = height / distancePerDegreeLatitude / 2; + const degLon = width / (distancePerDegreeLongitude * Math.cos(lat * Math.PI / 180)) / 2; + + // Rohy bez rotace (v relative souřadnicích) + const corners = [ + [-degLon, -degLat], + [ degLon, -degLat], + [ degLon, degLat], + [-degLon, degLat] + ]; + + // Rotace a posun + const rotated = corners.map(([dx, dy]) => { + const x = dx; + const y = dy; + + const rotatedX = x * Math.cos(rotation) - y * Math.sin(rotation); + const rotatedY = x * Math.sin(rotation) + y * Math.cos(rotation); + + return [lon + rotatedX, lat + rotatedY]; + }); + + // Uzavření polygonu + rotated.push(rotated[0]); + + return { + type: "Feature", + geometry: { + type: "Polygon", + coordinates: [rotated] + }, + properties: {} + };*/ diff --git a/Frontline.js b/Frontline.js new file mode 100644 index 0000000..088fafa --- /dev/null +++ b/Frontline.js @@ -0,0 +1,193 @@ +// 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]))); +} + +export const LEFT_SIDE = 1; +export const RIGHT_SIDE = 2; +export const BOTH_SIDES = 3; + +/** + * @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 density factor for the arrow's points. + * @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 {number} arrowData.protrusionSize - Length of the square protrusion from the offset side. + * + * @returns {{x: number, y: number}[]} - An array of points representing the arrow polygon. + */ +export function getFrontline(protrusionData) { + const style = protrusionData.style ?? LEFT_SIDE; + const splinePoints = computeSplinePoints(protrusionData.points, protrusionData.splineStep); + + let bodyPolygonLeft = []; + let bodyPolygonRight = []; + + if (style === BOTH_SIDES) { + const { + leftSidePoints: leftSidePointsLeftSide, + rightSidePoints: rightSidePointsLeftSide + } = computeSides(splinePoints, protrusionData.spacing, protrusionData.offsetDistance, LEFT_SIDE); + bodyPolygonLeft = [...leftSidePointsLeftSide, ...rightSidePointsLeftSide.reverse()]; + + const { + leftSidePoints: leftSidePointsRightSide, + rightSidePoints: rightSidePointsRightSide + } = computeSides(splinePoints, protrusionData.spacing, protrusionData.offsetDistance, RIGHT_SIDE); + bodyPolygonRight = [...leftSidePointsRightSide, ...rightSidePointsRightSide.reverse()]; + } + + const { leftSidePoints, rightSidePoints } = computeSides(splinePoints, protrusionData.spacing, protrusionData.offsetDistance, protrusionData.style); + + const bodyPolygon = [...leftSidePoints, ...rightSidePoints.reverse()]; + + let protrusionPolygons = []; + + + if (style === LEFT_SIDE) { + protrusionPolygons = computeProtrusion(leftSidePoints, protrusionData); + } else if (style === RIGHT_SIDE) { + protrusionPolygons = computeProtrusion(rightSidePoints, protrusionData); + } else if (style === BOTH_SIDES){ + let protrusionPolygonsLeft = computeProtrusion(leftSidePoints, protrusionData); + let protrusionPolygonsRight = computeProtrusion(rightSidePoints, protrusionData); + //protrusionPolygonsA = [...computeProtrusion(leftSidePoints, protrusionData), ...computeProtrusion(rightSidePoints, protrusionData)]; + return { + bodyLeft: bodyPolygonLeft, + bodyRight: bodyPolygonRight, + protrusionsLeft: protrusionPolygonsLeft, + protrusionsRight: protrusionPolygonsRight + }; + } + + return { + body: bodyPolygon, + protrusions: protrusionPolygons + }; +} + +function computeSplinePoints(points, density) { + 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 += density) { + splinePoints.push({ + x: cubicInterpolate([p0.x, p1.x, p2.x, p3.x], t), + y: cubicInterpolate([p0.y, p1.y, p2.y, p3.y], t) + }); + } + } + + return splinePoints; +} + +function computeSides(splinePoints, spacing, offsetDistance, style = LEFT_SIDE) { + let dots = []; + 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) { + const distanceX = currentPoint.y - previousPoint.y; + const distanceY = previousPoint.x - currentPoint.x; + const length = Math.hypot(distanceX, distanceY); + + let localOffsetDistance = offsetDistance; + + const offsetX = (distanceX / length) * localOffsetDistance; + const offsetY = (distanceY / length) * localOffsetDistance; + + dots.push({ x: currentPoint.x + offsetX, y: currentPoint.y + offsetY }); + dots.push({ x: currentPoint.x - offsetX, y: currentPoint.y - offsetY }); + + accumulatedDistance = 0; + + if (style === LEFT_SIDE) { + leftSidePoints.push({ x: currentPoint.x + offsetX, y: currentPoint.y + offsetY}); + rightSidePoints.push({ x: currentPoint.x, y: currentPoint.y}); + } else if (style === RIGHT_SIDE) { + leftSidePoints.push({ x: currentPoint.x, y: currentPoint.y}); + rightSidePoints.push({ x: currentPoint.x - offsetX, y: currentPoint.y - offsetY}); + } else if (style === BOTH_SIDES) { + leftSidePoints.push({ x: currentPoint.x + offsetX, y: currentPoint.y + offsetY }); + rightSidePoints.push({ x: currentPoint.x - offsetX, y: currentPoint.y - offsetY}); + } + } + } + + return { leftSidePoints, rightSidePoints }; +} + +function computeProtrusion(leftSidePoints, protrusionData) { + let 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 dx = p1.x - p0.x; + const dy = p1.y - p0.y; + const length = Math.hypot(dx, dy); + segments.push({ p0, p1, dx, dy, length }); + totalLength += length; + } + + const positions = []; + + for (let d = 0; d <= totalLength - (protrusionData.protrusionGap + protrusionData.protrusionStartSize); d += protrusionData.protrusionGap) { + positions.push(d + protrusionData.protrusionStartSize); + } + if (positions[positions.length - 1] < totalLength) { + positions.push(totalLength - protrusionData.protrusionStartSize); + } + + 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 t = localDistance / seg.length; + + const x = seg.p0.x + seg.dx * t; + const y = seg.p0.y + seg.dy * t; + + const nx = -seg.dy / seg.length; + const ny = seg.dx / seg.length; + + const centerX = x - nx * protrusionData.protrusionLength; + const centerY = y - ny * protrusionData.protrusionLength; + + const ux = seg.dx / seg.length; + const uy = seg.dy / seg.length; + + const corner1 = { x: x - ux * protrusionData.protrusionStartSize - nx * 0, y: y - uy * protrusionData.protrusionStartSize - ny * 0 }; + const corner2 = { x: x + ux * protrusionData.protrusionStartSize - nx * 0, y: y + uy * protrusionData.protrusionStartSize - ny * 0 }; + const corner3 = { x: centerX + ux * protrusionData.protrusionEndSize, y: centerY + uy * protrusionData.protrusionEndSize }; + const corner4 = { x: centerX - ux * protrusionData.protrusionEndSize, y: centerY - uy * protrusionData.protrusionEndSize }; + + protrusions.push([corner1, corner2, corner3, corner4]); + } + + return protrusions; +} diff --git a/Map.js b/Map.js new file mode 100644 index 0000000..0a0930c --- /dev/null +++ b/Map.js @@ -0,0 +1,740 @@ +mapboxgl.accessToken = 'pk.eyJ1Ijoib3V0ZG9vcm1hcHBpbmdjb21wYW55IiwiYSI6ImNqYmh3cDdjYzNsMnozNGxsYzlvMmk2bTYifQ.QqcZ4LVoLWnXafXdjZxnZg'; + const map = new mapboxgl.Map({ + container: 'map', + center: [20, 80], + zoom: 4 + }); + +import * as turf from "@turf/turf"; + +import { getArrowPolygon } from "./Arrow.js"; +import { ARROW_BODY_STYLE_CONSTANT, ARROW_BODY_STYLE_LINEAR, ARROW_BODY_STYLE_EXPONENTIAL } from "./Arrow.js"; +import { getCirclePolygon } from "./BasicShapes.js"; +import { getRectanglePolygon } from "./BasicShapes.js"; +import { getFrontline } from "./Frontline.js"; +import { LEFT_SIDE, RIGHT_SIDE, BOTH_SIDES } from "./Frontline.js"; +// Polygon merge using Turf library +import {mergeTurfPolygons} from "./Polygon.js"; +import {addTurfPolygonToMerge} from "./Polygon.js"; +import {toTurfPolygon} from "./Polygon.js"; + +const circleCenter = {x:320, y:180}; +const circleRadius = 70; +const circleDensity = 15; + +const circleCenterB = {x:400, y:280}; +const circleRadiusB = 70; +const circleDensityB = 15; + +const rectangleCenter= {x:100, y:300}; +const rectangleSideA = 70; +const rectangleSideB = 200; +const rectangleRotation = 40; + +const frontlinePointsA = [ + { x: 120, y: 400 }, + { x: 200, y: 100 }, + { x: 350, y: 200 }, + { x: 350, y: 400 }, + { x: 450, y: 480 }, + { x: 550, y: 440 }, + { x: 600, y: 300 }, +]; + +const frontlinePointsB = [ + { x: 420, y: 280 }, + { x: 430, y: 380 }, + { x: 500, y: 400 }, + { x: 520, y: 300 }, +]; + +const frontlinePointsC = [ + { x: 450, y: 200 }, + { x: 500, y: 250 }, + { x: 550, y: 250 }, + { x: 550, y: 200 } +]; + + +const frontlineDataA = { + points: frontlinePointsA, + splineStep: 0.08, + spacing: 10, + offsetDistance: 10, + protrusionLength: 15, + protrusionStartSize: 5, + protrusionEndSize: 2, + protrusionGap: 20, + style: LEFT_SIDE, + }; + + const frontlineDataB = { + points: frontlinePointsB, + splineStep: 0.02, + spacing: 10, + offsetDistance: 10, + protrusionLength: 15, + protrusionStartSize: 5, + protrusionEndSize: 5, + protrusionGap: 20, + style: RIGHT_SIDE, + }; + + const frontlineDataC = { + points: frontlinePointsC, + splineStep: 0.02, + spacing: 10, + offsetDistance: 10, + protrusionLength: 15, + protrusionStartSize: 5, + protrusionEndSize: 0, + protrusionGap: 20, + style: BOTH_SIDES, + }; + +/* +const arrowPolygonA = getArrowPolygon(arrowDataA, styleA, arrowHeadDataA); +const arrowPolygonB = getArrowPolygon(arrowDataB, styleB, arrowHeadDataB); +const arrowPolygonC = getArrowPolygon(arrowDataC, styleC); + +const circlePolygon = getCirclePolygon(circleCenter, circleRadius, circleDensity); +const circlePolygonB = getCirclePolygon(circleCenterB, circleRadiusB, circleDensityB); +const rectanglePolygon = getRectanglePolygon(rectangleCenter, rectangleSideA, rectangleSideB, rectangleRotation); + +const mergedTurfPoly = mergeTurfPolygons(arrowPolygonA, arrowPolygonC); +const mergedTurfPolyAll = addTurfPolygonToMerge(mergedTurfPoly, arrowPolygonB); + +const mergedTurfPolyRectangle = addTurfPolygonToMerge(mergedTurfPolyAll, rectanglePolygon); +const mergedRectangle = mergeTurfPolygons(circlePolygon, circlePolygonB); + +const rectanglePoly = getRectanglePolygon(circleCenter, rectangleSideA, rectangleSideB, rectangleRotation*-1); +const rectangleToTurfPoly = toTurfPolygon(rectanglePoly); + +const frontlinePolygonA = getFrontline(frontlineDataA); +let frontlinePolygonMergedA = mergeTurfPolygons(frontlinePolygonA.body,frontlinePolygonA.protrusions[0]); +for (let i = 1; i < frontlinePolygonA.protrusions.length; i++) +{ + frontlinePolygonMergedA = addTurfPolygonToMerge(frontlinePolygonMergedA, frontlinePolygonA.protrusions[i]); +} + +const frontlinePolygonB = getFrontline(frontlineDataB); +let frontlinePolygonMergedB = mergeTurfPolygons(frontlinePolygonB.body,frontlinePolygonB.protrusions[0]); +for (let i = 1; i < frontlinePolygonB.protrusions.length; i++) +{ + frontlinePolygonMergedB = addTurfPolygonToMerge(frontlinePolygonMergedB, frontlinePolygonB.protrusions[i]); +} + +const frontlinePolygonC = getFrontline(frontlineDataC); + +let frontlinePolygonMergedLeft = mergeTurfPolygons(frontlinePolygonC.bodyLeft, frontlinePolygonC.protrusionsLeft[0]); +for (let i = 1; i < frontlinePolygonC.protrusionsLeft.length; i++) { + frontlinePolygonMergedLeft = addTurfPolygonToMerge(frontlinePolygonMergedLeft, frontlinePolygonC.protrusionsLeft[i]); +} + +let frontlinePolygonMergedRight = mergeTurfPolygons(frontlinePolygonC.bodyRight, frontlinePolygonC.protrusionsRight[0]); +for (let i = 1; i < frontlinePolygonC.protrusionsRight.length; i++) { + frontlinePolygonMergedRight = addTurfPolygonToMerge(frontlinePolygonMergedRight, frontlinePolygonC.protrusionsRight[i]); +}*/ + +//const circleGeoJSON = getCircleGeoJSON({x: 20, y: 80}, 2, 50, map); + +const pointsB = [ + { x: 70, y: 38 }, + { x: 71, y: 45}, + { x: 65, y: 50 }, + { x: 70, y: 53} +]; + +const arrowDataB = { + points: pointsB, + splineStep: 0.01, + spacing: 0.01, + offsetDistance: 1 + }; + const styleB = { + calculation: ARROW_BODY_STYLE_LINEAR, + range: 1, + minValue: 0.1 + }; + const arrowHeadDataB = { + widthArrow: 1, + lengthArrow: 1 + }; + +const arrowPolygonB = getArrowPolygon(arrowDataB, styleB, arrowHeadDataB); + const pointsC= [ + { x: 50, y: 38 }, + { x: 51, y: 45}, + { x: 45, y: 50 }, + { x: 48, y: 55} +]; + +const arrowDataC = { + points: pointsC, + splineStep: 0.02, + spacing: 1, + offsetDistance: 1 + }; + const styleC = { + calculation: ARROW_BODY_STYLE_LINEAR, + range: 1, + minValue: 0.1 + }; + const arrowHeadDataC = { + widthArrow: 1, + lengthArrow: 1 + }; + +const arrowPolygonC = getArrowPolygon(arrowDataC, styleC, arrowHeadDataC); + +const points = [ + { x: 80, y: 20 }, + { x: 81, y: 22}, + { x: 82, y: 28 }, + { x: 81, y: 30}, + { x: 80, y: 20} +]; + +// Turf polygon +const turfPolygon = turf.polygon([[ + [20, 80], + [22, 81], + [28, 82], + [30, 81], + [20, 80] + ]]); + + const pointsA= [ + { x: 80, y: 38 }, + { x: 81, y: 45}, + { x: 75, y: 50 }, + { x: 78, y: 55} +]; + +const arrowDataA = { + points: pointsA, + splineStep: 0.2, + spacing: 3, + offsetDistance: 1 + }; + const styleA = { + calculation: ARROW_BODY_STYLE_LINEAR, + range: 1, + minValue: 0.1 + }; + const arrowHeadDataA = { + widthArrow: 1, + lengthArrow: 1 + }; + +const arrowPolygonA = getArrowPolygon(arrowDataA, styleA, arrowHeadDataA); + +const latLonGrid = generateLatLonGrid(5); + + +function createIsoscelesTriangle(center, baseLengthMeters, heightMeters, bearing = 0) { + const halfBase = baseLengthMeters / 2; + + // Výpočet bodů základny (levý a pravý bod) + const leftBase = turf.destination(center, halfBase, bearing - 90, { units: 'meters' }); + const rightBase = turf.destination(center, halfBase, bearing + 90, { units: 'meters' }); + + // Výpočet vrcholu trojúhelníku – směr daný bearing (nahoru např. 0°) + const apex = turf.destination(center, heightMeters, bearing, { units: 'meters' }); + + const triangle = turf.polygon([[ + leftBase.geometry.coordinates, + rightBase.geometry.coordinates, + apex.geometry.coordinates, + leftBase.geometry.coordinates // Uzavření polygonu + ]]); + + return triangle; +} + + + + + +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 interpolatePointsCatmullRom(points, segments = 10) { + if (points.length < 2) return points; + + const extended = [points[0], ...points, points[points.length - 1]]; + const interpolated = []; + + for (let i = 1; i < extended.length - 2; i++) { + for (let j = 0; j < segments; j++) { + const t = j / segments; + const lon = cubicInterpolate([extended[i - 1][0], extended[i][0], extended[i + 1][0], extended[i + 2][0]], t); + const lat = cubicInterpolate([extended[i - 1][1], extended[i][1], extended[i + 1][1], extended[i + 2][1]], t); + interpolated.push([lon, lat]); + } + } + interpolated.push(points[points.length - 1]); + return interpolated; + } + function computeSideOffsets(points, offsetMeters) { + const left = []; + const right = []; + + for (let i = 1; i < points.length; i++) { + const prev = points[i - 1]; + const curr = points[i]; + const bearing = turf.bearing(turf.point(prev), turf.point(curr)); + + const leftOffset = turf.destination(turf.point(curr), offsetMeters, bearing - 90, { units: 'meters' }); + const rightOffset = turf.destination(turf.point(curr), offsetMeters, bearing + 90, { units: 'meters' }); + + left.push(leftOffset.geometry.coordinates); + right.push(rightOffset.geometry.coordinates); + } + + return { left, right }; + } + + function createIsoscelesTriangleCoords(center, baseLengthMeters, heightMeters, bearing) { + 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; + console.log("Aktuální bod:", left); + console.log("Aktuální bod:", right); + console.log("Aktuální bod:", tip); + return [left, right, tip]; + } + + function drawArrowPolygon(map, basePoints, offset = 10000) { + const smooth = interpolatePointsCatmullRom(basePoints, 20); + const { leftSidePoints, rightSidePoints } = computeSidesWGS84(smooth, offset); + + console.log("Aktuální bod:", tip); + const end = smooth[smooth.length - 1]; + const prev = smooth[smooth.length - 2]; + const bearing = turf.bearing(turf.point(prev), turf.point(end)); + + const triangleCoords = createIsoscelesTriangleCoords(turf.point(end), offset * 2, offset * 3, bearing); + + const polygonCoords = [ + ...leftSidePoints, + ...triangleCoords, + ...rightSidePoints.reverse(), + leftSidePoints[0] + ]; + + const fullPolygon = turf.polygon([[...polygonCoords]]); + + 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 + } + }); + } + +function computeSidesWGS84(points, offsetDistanceMeters = 1000) { + const leftSidePoints = []; + const rightSidePoints = []; + + + for (let i = 1; i < points.length; i++) { + const prev = points[i - 1]; + const curr = points[i]; + + const bearing = turf.bearing(turf.point(prev), turf.point(curr)); + const leftBearing = bearing - 90; + const rightBearing = bearing + 90; + + const leftPoint = turf.destination(turf.point(curr), offsetDistanceMeters, leftBearing, { units: 'meters' }); + const rightPoint = turf.destination(turf.point(curr), offsetDistanceMeters, rightBearing, { units: 'meters' }); + + leftSidePoints.push(leftPoint.geometry.coordinates); + rightSidePoints.push(rightPoint.geometry.coordinates); + } + + // První bod + const first = points[0]; + const second = points[1]; + const initialBearing = turf.bearing(turf.point(first), turf.point(second)); + const leftInitial = turf.destination(turf.point(first), offsetDistanceMeters, initialBearing - 90, { units: 'meters' }); + const rightInitial = turf.destination(turf.point(first), offsetDistanceMeters, initialBearing + 90, { units: 'meters' }); + + leftSidePoints.unshift(leftInitial.geometry.coordinates); + rightSidePoints.unshift(rightInitial.geometry.coordinates); + + console.log("Aktuální bod:", leftSidePoints); + console.log("Aktuální bod:", rightSidePoints); + return { + leftSidePoints, + rightSidePoints + }; +} + +function drawSideLines(map, sides, prefix = "arrow-side") { + map.addSource(`${prefix}-left`, { + type: "geojson", + data: turf.lineString(sides.leftSidePoints) + }); + + map.addLayer({ + id: `${prefix}-left`, + type: "line", + source: `${prefix}-left`, + paint: { + "line-color": "blue", + "line-width": 2 + } + }); + + map.addSource(`${prefix}-right`, { + type: "geojson", + data: turf.lineString(sides.rightSidePoints) + }); + + map.addLayer({ + id: `${prefix}-right`, + type: "line", + source: `${prefix}-right`, + paint: { + "line-color": "green", + "line-width": 2 + } + }); +} + +function drawSmoothLineThroughPoints(map, points, lineId = "smooth-line") { + const smoothPoints = interpolatePointsCatmullRom(points, 20); + const line = turf.lineString(smoothPoints); + + map.addSource(lineId, { + type: "geojson", + data: line + }); + + map.addLayer({ + id: lineId, + type: "line", + source: lineId, + paint: { + "line-color": "red", + "line-width": 3 + } + }); + + // Boční linie + const sides = computeSidesWGS84(smoothPoints, 10000); // 10 km offset + drawSideLines(map, sides, lineId + "-sides"); + + // Šipka na konci + const lastPoint = smoothPoints[smoothPoints.length - 1]; + const secondLastPoint = smoothPoints[smoothPoints.length - 2]; + const bearing = turf.bearing(turf.point(secondLastPoint), turf.point(lastPoint)); + + const triangle = createIsoscelesTriangle(turf.point(lastPoint), 60000, 80000, bearing); // 20km základna, 30km výška + + map.addSource(lineId + "-arrow", { + type: "geojson", + data: triangle + }); + + map.addLayer({ + id: lineId + "-arrow", + type: "fill", + source: lineId + "-arrow", + paint: { + "fill-color": "#ff0000", + "fill-opacity": 0.7 + } + }); + } + +map.on('load', () => { + +const points = [ + [14.42076, 50.08804], // Praha + [15.0, 50.0], + [16.3725, 48.2082], // Vídeň + [17.0, 49.0], + [13.4050, 52.52] // Berlín + ]; + + + + + function createIsoscelesTriangle(center, baseLengthMeters, heightMeters, bearing = 0) { + const halfBase = baseLengthMeters / 2; + + 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, bearing, { units: 'meters' }); + + return turf.polygon([[ + leftBase.geometry.coordinates, + rightBase.geometry.coordinates, + apex.geometry.coordinates, + leftBase.geometry.coordinates + ]]); + } + + + + + + const center = turf.point([52.95, 69.95]); // výchozí střed základny + + // Vytvoř polygon trojúhelníku + const triangle = createIsoscelesTriangle(center, 40000, 100000, 10); // 2 km základna, 1 km výška, směr 0° (na sever) + + map.addSource("triangle", { + type: "geojson", + data: triangle + }); + + map.addLayer({ + id: "triangle", + type: "fill", + source: "triangle", + paint: { + "fill-color": "purple", + "fill-opacity": 0.5 + } + }); + + map.addLayer({ + id: "triangle-outline", + type: "line", + source: "triangle", + paint: { + "line-color": "#000", + "line-width": 2 + } + }); + + map.addSource("latLonGrid", { + type: "geojson", + data: latLonGrid +}); + +map.addLayer({ + id: "latLonGrid", + type: "line", + source: "latLonGrid", + layout: {}, + paint: { + "line-color": "#888", + "line-width": 1, + "line-opacity": 0.5 + } +}); + + // Generuj GeoJSON pro kruh + const circleGeoJSON = getCirclePolygon([20, 80], 120, 20); + const circle = turf.circle([20, 80], 120000, { units: "meters", steps: 64 }); + const rectangleGeoJSON = getRectanglePolygon([20, 80], 2200, 2200); + const fsdafds = toTurfPolygon(points); + const arrowBGeoJSON = toTurfPolygon(arrowPolygonB); + const arrowCGeoJSON = toTurfPolygon(arrowPolygonC); + const arrowAGeoJSON = toTurfPolygon(arrowPolygonA); + + //console.log(JSON.stringify(arrowAGeoJSON, null, 2)); + // Arrow + // Přidání GeoJSON jako zdroj + map.addSource("arrowPolygonA", { + type: "geojson", + data: arrowAGeoJSON + }); + + // Vrstva pro výplň polygonu + map.addLayer({ + id: "arrowPolygonA", + type: "fill", + source: "arrowPolygonA", + layout: {}, + paint: { + "fill-color": "pink", + "fill-opacity": 0.6 + } + }); + + // Vrstva pro obrys polygonu + map.addLayer({ + id: "arrowPolygonA-outline", + type: "line", + source: "arrowPolygonA", + paint: { + "line-color": "#000", + "line-width": 3 + } + }); + + // Arrow + // Přidání GeoJSON jako zdroj + map.addSource("arrowPolygonB", { + type: "geojson", + data: arrowBGeoJSON + }); + + // Vrstva pro výplň polygonu + map.addLayer({ + id: "arrowPolygonB", + type: "fill", + source: "arrowPolygonB", + layout: {}, + paint: { + "fill-color": "yellow", + "fill-opacity": 0.6 + } + }); + + // Vrstva pro obrys polygonu + map.addLayer({ + id: "arrowPolygonB-outline", + type: "line", + source: "arrowPolygonB", + paint: { + "line-color": "#000", + "line-width": 3 + } + }); + + // Arrow + // Přidání GeoJSON jako zdroj + map.addSource("arrowPolygonC", { + type: "geojson", + data: arrowCGeoJSON + }); + + // Vrstva pro výplň polygonu + map.addLayer({ + id: "arrowPolygonC", + type: "fill", + source: "arrowPolygonC", + layout: {}, + paint: { + "fill-color": "yellow", + "fill-opacity": 0.6 + } + }); + + // Vrstva pro obrys polygonu + map.addLayer({ + id: "arrowPolygonC-outline", + type: "line", + source: "arrowPolygonC", + paint: { + "line-color": "#000", + "line-width": 3 + } + }); + + // CIRCLE + // Přidání GeoJSON jako zdroj + map.addSource("circlePolygon", { + "type": "geojson", + "data": circleGeoJSON + }); + + // Přidání vrstvy pro vykreslení polygonu + map.addLayer({ + "id": "circlePolygon", + "type": "fill", + "source": "circlePolygon", + "layout": {}, + "paint": { + "fill-color": "blue", + "fill-opacity": 0.6 + } + }); + + // Přidání outline pro polygon + map.addLayer({ + "id": "circlePolygon-outline", + "type": "line", + "source": "circlePolygon", + "paint": { + "line-color": "#000", + "line-width": 3 + } + }); + + // 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 + } + }); +}); + + + +function generateLatLonGrid(step = 10) { + const features = []; + + // Rovnoběžky (latitudes) + 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]) + }, + properties: { + type: "latitude", + value: lat + } + }); + } + + // Poledníky (longitudes) + 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]) + }, + properties: { + type: "longitude", + value: lon + } + }); + } + + return { + type: "FeatureCollection", + features + }; +} \ No newline at end of file diff --git a/MapArrow.js b/MapArrow.js new file mode 100644 index 0000000..30b7d6e --- /dev/null +++ b/MapArrow.js @@ -0,0 +1,205 @@ +mapboxgl.accessToken = 'pk.eyJ1Ijoib3V0ZG9vcm1hcHBpbmdjb21wYW55IiwiYSI6ImNqYmh3cDdjYzNsMnozNGxsYzlvMmk2bTYifQ.QqcZ4LVoLWnXafXdjZxnZg'; + const map = new mapboxgl.Map({ + container: 'map', + center: [10, 50], + zoom: 5 + }); + +import * as turf from "@turf/turf"; +import { ARROW_BODY_STYLE_CONSTANT, ARROW_BODY_STYLE_LINEAR, ARROW_BODY_STYLE_EXPONENTIAL } from "./Arrow.js"; + +map.on('load', () => { + const points = [ + [1.42076, 40.08804], + [358.4050, 50.52] + ]; + + const fullPolygon = getArrowPolygon(points, 20000); // offset 20 km + 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 + } + }); + + 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 + } + }); + + // Obrys (černý) + map.addLayer({ + id: "arrow-outline", + type: "line", + source: "arrow-shape", + paint: { + "line-color": "#000000", + "line-width": 2, + "line-opacity": 1 + } + }); +}); + +const points = [ + { x: 70, y: 38 }, + { x: 71, y: 45}, + { x: 65, y: 50 }, + { x: 70, y: 53} +]; + +const arrowData = { + points: points, + splineStep: 0.01, + spacing: 0.01, + offsetDistance: 10000 + }; + const style = { + calculation: ARROW_BODY_STYLE_LINEAR, + range: 1, + minValue: 0.1 + }; + const arrowHeadData = { + widthArrow: 1, + lengthArrow: 1 + }; + +// 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]))); +} + +/** + * @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) { + const splineStep = 20; + const smooth = computeSplinePoints(arrowData.points, splineStep); + const { leftSidePoints, rightSidePoints } = computeSideOffsets(smooth, arrowData.offsetDistance); + + const end = smooth[smooth.length -1]; + const bearing = averageBearing(smooth, 3); + const triangle = createIsoscelesTriangleCoords(turf.point(end), arrowData.offsetDistance * 5, arrowData.offsetDistance * 5, bearing); + + const polygonCoords = [ + ...leftSidePoints, + ...triangle, + ...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); + } + } + // Průměr s korekcí kruhového rozsahu + 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, segments = 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 < segments; j++) { + const t = j / segments; + 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) { + let leftSidePoints = []; + let rightSidePoints = []; + + 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)); + leftSidePoints.push(turf.destination(turf.point(currentPoint), offsetMeters, bearing - 90, { units: 'meters' }).geometry.coordinates); + rightSidePoints.push(turf.destination(turf.point(currentPoint), offsetMeters, 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]; +} + +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 + }; +} \ No newline at end of file diff --git a/Polygon.js b/Polygon.js new file mode 100644 index 0000000..faa7719 --- /dev/null +++ b/Polygon.js @@ -0,0 +1,79 @@ +import * as turf from "@turf/turf"; + +/** + * Converts an array of canvas-compatible points into a Turf.js Polygon. + * + * @param {{x: number, y: number}[]} points - Array of points with x and y properties. + * + * @returns {import('@turf/turf').Feature | null} A Turf.js Polygon feature, or null if input is invalid. + */ +export function toTurfPolygon(points) { + if (!points || points.length < 3) { + console.error("Invalid input for polygon:", points); + return null; + } + + const coords = points.map(p => [p.y, p.x]); + coords.push(coords[0]); + return turf.polygon([coords]); +} + +/** + * Merges two polygons (in canvas format) into a single Turf.js polygon using turf.union. + * + * @param {{x: number, y: number}[]} polygonA - First polygon (array of points). + * @param {{x: number, y: number}[]} polygonB - Second polygon (array of points). + * + * @returns {import('@turf/turf').Feature | null} A merged Turf.js polygon, or null on failure. + */ +export function mergeTurfPolygons(polygonA, polygonB) { + const turfPolygonA = toTurfPolygon(polygonA); + const turfPolygonB = toTurfPolygon(polygonB); + + return turf.union(turf.featureCollection([turfPolygonA, turfPolygonB])); +} + +/** + * Adds a new polygon to an existing merged Turf.js polygon. + * + * @param {import('@turf/turf').Feature} polygonA - Existing merged Turf.js polygon. + * @param {{x: number, y: number}[]} polygonB - New polygon in canvas point format to add to the merge. + * + * @returns {import('@turf/turf').Feature} Updated merged Turf.js polygon. + */ +export function addTurfPolygonToMerge(polygonA, polygonB) { + const testB = toTurfPolygon(polygonB); + return turf.union(turf.featureCollection([polygonA, testB])); +} + +/** + * @param {{x: number, y: number}[][]} polygons + * @returns {Feature} + */ + +export function mergePolygons(polygons) { + + if (!polygons || polygons.length === 0) + return undefined; + + if (polygons.length === 1) + return toTurfPolygon(polygons[0]); + + return turf.union(turf.featureCollection(polygons.map(p => toTurfPolygon(p)))); +} + +/** + * + * @param {Array>|undefined} features + * @returns {Feature|undefined|*} + */ +export function mergePolygonFeatures(features) { + + if (!features || features.length === 0) + return undefined; + + if (features.length === 1) + return features[0]; + + return turf.union(turf.featureCollection(features)); +} \ No newline at end of file diff --git a/PolygonVisuals.js b/PolygonVisuals.js new file mode 100644 index 0000000..c62c947 --- /dev/null +++ b/PolygonVisuals.js @@ -0,0 +1,45 @@ +// Converts a Turf polygon into an array of canvas-compatible points +function toCanvasPolygon(turfPolygon) { + if (!turfPolygon || !turfPolygon.geometry) return []; + + let polygons = []; + + if (turfPolygon.geometry.type === 'Polygon') { + polygons.push(turfPolygon.geometry.coordinates[0]); + } + else if (turfPolygon.geometry.type === 'MultiPolygon') { + turfPolygon.geometry.coordinates.forEach(polygon => { + polygons.push(polygon[0]); + }); + } + else { + console.error("Unsupported geometry type:", turfPolygon.geometry.type); + return []; + } + + return polygons.map(coords => + coords.slice(0, -1).map(coord => ({ x: coord[0], y: coord[1] })) + ); +} + +// Draws a polygon on the canvas with a given color +export function drawPolygon(turfPolygon, color, canvas) { + const ctx = canvas.getContext("2d"); + ctx.fillStyle = color; + + const polygons = toCanvasPolygon(turfPolygon); + if (!polygons.length) { + console.log("No valid polygons to draw."); + return; + } + + polygons.forEach(points => { + if (points.length < 3) return; + + ctx.beginPath(); + ctx.moveTo(points[0].x, points[0].y); + points.forEach(p => ctx.lineTo(p.x, p.y)); + ctx.closePath(); + ctx.fill(); + }); +} \ No newline at end of file diff --git a/index.html b/index.html index 52311e9..d829670 100644 --- a/index.html +++ b/index.html @@ -1,14 +1,19 @@ - - - - - - - Vite App - - -
- - - - + + + + +Display a map on a webpage + + + + + + +
+ + + + \ No newline at end of file