diff --git a/Arrow.js b/Arrow.js index f9a0d3f..9cdb650 100644 --- a/Arrow.js +++ b/Arrow.js @@ -2,43 +2,144 @@ 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)); +//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]))); } -// 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 - }; +function exponentialWidthCurve(normalizedPosition, range = 5, minValue = 0.1) { + return minValue + (1 - minValue) * Math.exp(-range * normalizedPosition); } -// 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) - }; +function linearWidthCurve(normalizedPosition, range = 1, minValue = 0.1) { + return 1 + (minValue - 1) * normalizedPosition / range ; } +//GEOJSON +/** + * @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.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 {GeoJSON.Feature} - 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]; +} +//GEOJSON +//CANVAS /** * @param {Object} arrowData - Object with data for arrow * @param {{x: number, y: number}[]} arrowData.points - List of points defining the arrow's path. @@ -57,7 +158,7 @@ function metersToLonLat(mx, my) { * * @returns {{x: number, y: number}[]} - An array of points representing the arrow polygon. */ -export function getArrowPolygon( +export function getArrowPolygonEuclidean( arrowData, style= undefined, arrowHeadData = undefined) { @@ -69,17 +170,16 @@ export function getArrowPolygon( minValue: 0 }; - const splinePoints = computeSplinePoints(arrowData.points, arrowData.splineStep); - const { leftSidePoints, rightSidePoints } = computeSides(splinePoints, arrowData.spacing, arrowData.offsetDistance, style); + const splinePoints = computeSplinePointsEuclidean(arrowData.points, arrowData.splineStep); + const { leftSidePoints, rightSidePoints } = computeSidesEuclidean(splinePoints, arrowData.spacing, arrowData.offsetDistance, style); const arrowHead= arrowHeadData - ? createIsoscelesTriangleFromSpline(splinePoints, arrowHeadData.widthArrow, arrowHeadData.lengthArrow) + ? computeArrowHeadEuclidean(splinePoints, arrowHeadData.widthArrow, arrowHeadData.lengthArrow) : []; - const fullPolygon = [...leftSidePoints, ...arrowHead.reverse(), ...rightSidePoints.reverse()]; - return fullPolygon; + return [...leftSidePoints, ...arrowHead.reverse(), ...rightSidePoints.reverse()]; } -function computeSplinePoints(points, splineStep) { +function computeSplinePointsEuclidean(points, splineStep) { let splinePoints = []; for (let i = 0; i < points.length - 1; i++) { @@ -94,11 +194,11 @@ function computeSplinePoints(points, splineStep) { }); } } - splinePoints.x /100; + return splinePoints; } -function computeSides(splinePoints, spacing, offsetDistance, style) { +function computeSidesEuclidean(splinePoints, spacing, offsetDistance, style) { let leftSidePoints = []; let rightSidePoints = []; @@ -112,17 +212,8 @@ function computeSides(splinePoints, spacing, offsetDistance, style) { 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 distanceX = currentPoint.y - previousPoint.y; + const distanceY = previousPoint.x - currentPoint.x; const length = Math.hypot(distanceX, distanceY); const normalizedPosition = i / (splinePoints.length - 1); @@ -144,66 +235,29 @@ function computeSides(splinePoints, spacing, offsetDistance, style) { 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 }); + 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) { +function computeArrowHeadEuclidean(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 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}, + { 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 ; -} +//CANVAS +//ARROW diff --git a/BasicShapes.js b/BasicShapes.js index 053f814..ff0c457 100644 --- a/BasicShapes.js +++ b/BasicShapes.js @@ -1,9 +1,11 @@ -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'; +//CIRCLE +const distancePerDegreeLongitude = 111.320; // 2π×6378.1km/360 +const distancePerDegreeLatitude = 110.574; // 2π×6356.75km/360 + +//GEOJSON /** * @param {Object} center - The center point of the circle. * @param {number} center.x @@ -11,7 +13,7 @@ import { toMercator, toWgs84 } from '@turf/projection'; * @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. + * @returns {GeoJSON.Feature} GeoJSON Feature representing the circle polygon. */ export function getCirclePolygon(center, radius, density = 64) { const points = []; @@ -34,38 +36,58 @@ export function getCirclePolygon(center, radius, density = 64) { // Close the circle by adding the first point again points.push(points[0]); + return turf.polygon([[...points]]); return { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [points] + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [points] }, - properties: {} + "properties": {} }; } - -function getDistancePerDegreeLongitude(latitude) { - return 111.320 * Math.cos(latitude * Math.PI / 180); +//GEOJSON +//CANVAS +/** + * @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 {{x: number, y: number}[]} An array of points representing the vertices of the circle polygon. + */ +export function getCirclePolygonEuclidean(center, radius, density) { + const points = []; + for (let i = 0; i < density; i++) { + const angle = (i / density) * Math.PI * 2; + const x = center.x + radius * Math.cos(angle); + const y = center.y + radius * Math.sin(angle); + points.push({ x, y }); + } + return points; } +//CANVAS +//CIRCLE +//RECTANGLE +//GEOJSON /** * @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} 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. + * @returns {GeoJSON.Feature} 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; @@ -76,13 +98,11 @@ export function getRectanglePolygon(center, width, height, rotation = 0) { [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])); @@ -98,40 +118,35 @@ function rotateXY(x, y, cx, cy, angleRad) { 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) +//GEOJSON +//CANVAS +/** + * @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 {{x: number, y: number}[]} An array of points representing the vertices of the rectangle polygon. + */ +export function getRectanglePolygonEuclidean(center, sideA, sideB, rotation) { + const halfA = sideA / 2; + const halfB = sideB / 2; + const corners = [ - [-degLon, -degLat], - [ degLon, -degLat], - [ degLon, degLat], - [-degLon, degLat] + { x: -halfA, y: -halfB }, + { x: halfA, y: -halfB }, + { x: halfA, y: halfB }, + { x: -halfA, y: halfB } ]; - - // 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]; + + return corners.map(point => { + return { + x: center.x + point.x * Math.cos(rotation) - point.y * Math.sin(rotation), + y: center.y + point.x * Math.sin(rotation) + point.y * Math.cos(rotation) + }; }); - - // Uzavření polygonu - rotated.push(rotated[0]); - - return { - type: "Feature", - geometry: { - type: "Polygon", - coordinates: [rotated] - }, - properties: {} - };*/ +} +//CAVNAS +//RECTANGLE \ No newline at end of file diff --git a/MapArrow.js b/MapArrow.js deleted file mode 100644 index dc1f95e..0000000 --- a/MapArrow.js +++ /dev/null @@ -1,395 +0,0 @@ -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 \ No newline at end of file diff --git a/MapPolygons.js b/MapPolygons.js new file mode 100644 index 0000000..021dd69 --- /dev/null +++ b/MapPolygons.js @@ -0,0 +1,199 @@ +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"; + +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 rectangleGeoJSON = getRectanglePolygon([20, 80], 2200, 2200); + const rectangleBGeoJSON = getRectanglePolygon([20, 20], 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 + }); + + map.addLayer({ + "id": "rectanglePolygon", + "type": "fill", + "source": "rectanglePolygon", + "layout": {}, + "paint": { + "fill-color": "red", + "fill-opacity": 0.6 + } + }); + + map.addLayer({ + "id": "rectanglePolygon-outline", + "type": "line", + "source": "rectanglePolygon", + "paint": { + "line-color": "#000", + "line-width": 3 + } + }); + + map.addSource("rectangleBPolygon", { + "type": "geojson", + "data": rectangleBGeoJSON + }); + + map.addLayer({ + "id": "rectangleBPolygon", + "type": "fill", + "source": "rectangleBPolygon", + "layout": {}, + "paint": { + "fill-color": "red", + "fill-opacity": 0.6 + } + }); + + map.addLayer({ + "id": "rectangleBPolygon-outline", + "type": "line", + "source": "rectangleBPolygon", + "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, + 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 + +//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 \ No newline at end of file diff --git a/index.html b/index.html index d829670..3a82005 100644 --- a/index.html +++ b/index.html @@ -13,7 +13,6 @@ body { margin: 0; padding: 0; }
- - + \ No newline at end of file