mirror of
https://github.com/orangecoding/fredy.git
synced 2026-06-16 12:31:07 +00:00
131 lines
4.8 KiB
JavaScript
131 lines
4.8 KiB
JavaScript
/*
|
|
* Copyright (c) 2026 by Christian Kellner.
|
|
* Licensed under Apache-2.0 with Commons Clause and Attribution/Naming Clause
|
|
*/
|
|
|
|
/**
|
|
* Calculates the great-circle distance between two points on a sphere using the Haversine formula.
|
|
*
|
|
* I'm using the Haversine formula here because it accounts for the Earth's curvature.
|
|
* By calculating the central angle (c) between two points and multiplying it by the Earth's radius (R ≈ 6371km),
|
|
* we get a pretty accurate straight-line distance. It's basically some trigonometry involving
|
|
* sines and cosines of the latitudes and longitudes to find the chord length (a) first.
|
|
*
|
|
* @param {number} lat1 - Latitude of the first point
|
|
* @param {number} lon1 - Longitude of the first point
|
|
* @param {number} lat2 - Latitude of the second point
|
|
* @param {number} lon2 - Longitude of the second point
|
|
* @returns {number} Distance in meters, rounded to one decimal place
|
|
*/
|
|
export const distanceMeters = (lat1, lon1, lat2, lon2) => {
|
|
const R = 6371000;
|
|
const toRad = (deg) => (deg * Math.PI) / 180;
|
|
|
|
const phi1 = toRad(lat1);
|
|
const phi2 = toRad(lat2);
|
|
const dPhi = toRad(lat2 - lat1);
|
|
const dLambda = toRad(lon2 - lon1);
|
|
|
|
const a =
|
|
Math.sin(dPhi / 2) * Math.sin(dPhi / 2) +
|
|
Math.cos(phi1) * Math.cos(phi2) * Math.sin(dLambda / 2) * Math.sin(dLambda / 2);
|
|
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
|
|
return Math.round(R * c * 10) / 10;
|
|
};
|
|
|
|
/**
|
|
* Generates an array of coordinates representing a circle on a map.
|
|
*
|
|
* To get this circle right, I'm approximating it with a polygon of 64 points.
|
|
* Since the Earth isn't flat, I have to adjust the longitude distance based on the latitude
|
|
* using the cosine of the latitude. The formula for the points is basically:
|
|
* x = center_lon + radius_lon * cos(theta)
|
|
* y = center_lat + radius_lat * sin(theta)
|
|
* where theta ranges from 0 to 2π. This handles the slight "squishing" of distances as you move away from the equator.
|
|
*
|
|
* @param {number[]} center - [longitude, latitude] of the center
|
|
* @param {number} radiusInKm - Radius of the circle in kilometers
|
|
* @param {number} [points=64] - Number of points to generate for the polygon
|
|
* @returns {number[][]} Array of [longitude, latitude] coordinates
|
|
*/
|
|
export const generateCircleCoords = (center, radiusInKm, points = 64) => {
|
|
const [longitude, latitude] = center;
|
|
const coords = [];
|
|
|
|
// 1 degree of latitude is roughly 110.574 km
|
|
// 1 degree of longitude is roughly 111.32 km * cos(latitude)
|
|
const distanceX = radiusInKm / (111.32 * Math.cos((latitude * Math.PI) / 180));
|
|
const distanceY = radiusInKm / 110.574;
|
|
|
|
for (let i = 0; i < points; i++) {
|
|
const theta = (i / points) * (2 * Math.PI);
|
|
const x = distanceX * Math.cos(theta);
|
|
const y = distanceY * Math.sin(theta);
|
|
coords.push([longitude + x, latitude + y]);
|
|
}
|
|
// Close the polygon
|
|
coords.push(coords[0]);
|
|
|
|
return coords;
|
|
};
|
|
|
|
/**
|
|
* Calculates the bounding box for a given center and radius.
|
|
*
|
|
* I'm calculating the bounds by offsetting the center coordinates by the radius.
|
|
* Again, using the 110.574 km per degree latitude and the cosine-adjusted longitude
|
|
* to make sure the bounds actually contain the circle, even at our latitudes.
|
|
* I've added a bit of padding (15% by default) to make sure everything fits nicely on the screen.
|
|
*
|
|
* @param {number[]} center - [longitude, latitude] of the center
|
|
* @param {number} radiusInKm - Radius in kilometers
|
|
* @param {number} [padding=0.15] - Percentage of padding to add
|
|
* @returns {number[][]} Bounding box coordinates [[minLon, minLat], [maxLon, maxLat]]
|
|
*/
|
|
export const getBoundsFromCenter = (center, radiusInKm, padding = 0.15) => {
|
|
const [lng, lat] = center;
|
|
const kmInDegLat = 1 / 110.574;
|
|
const kmInDegLng = 1 / (111.32 * Math.cos((lat * Math.PI) / 180));
|
|
|
|
const offsetLng = radiusInKm * kmInDegLng * (1 + padding);
|
|
const offsetLat = radiusInKm * kmInDegLat * (1 + padding);
|
|
|
|
return [
|
|
[lng - offsetLng, lat - offsetLat],
|
|
[lng + offsetLng, lat + offsetLat],
|
|
];
|
|
};
|
|
|
|
/**
|
|
* Calculates the bounding box for a set of coordinates.
|
|
*
|
|
* @param {number[][]} coords - Array of [longitude, latitude] coordinates
|
|
* @param {number} [padding=0.1] - Padding to add to the bounds
|
|
* @returns {number[][]} Bounding box coordinates [[minLon, minLat], [maxLon, maxLat]]
|
|
*/
|
|
export const getBoundsFromCoords = (coords, padding = 0.1) => {
|
|
if (!coords || coords.length === 0) return null;
|
|
|
|
let minLng = Infinity;
|
|
let minLat = Infinity;
|
|
let maxLng = -Infinity;
|
|
let maxLat = -Infinity;
|
|
|
|
coords.forEach(([lng, lat]) => {
|
|
if (lng < minLng) minLng = lng;
|
|
if (lng > maxLng) maxLng = lng;
|
|
if (lat < minLat) minLat = lat;
|
|
if (lat > maxLat) maxLat = lat;
|
|
});
|
|
|
|
const lngDiff = maxLng - minLng;
|
|
const latDiff = maxLat - minLat;
|
|
|
|
return [
|
|
[minLng - lngDiff * padding, minLat - latDiff * padding],
|
|
[maxLng + lngDiff * padding, maxLat + latDiff * padding],
|
|
];
|
|
};
|