Ctrl K
ComunidadGuías…

src/lib/geo-distance.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// How far apart two recorded positions are, and how to say it.
//
// This exists because the read-only map cannot show it. That map is capped at
// zoom 10 (see DECISIONS.md), where a hundred metres is less than a pixel, so
// an entry and an exit fix a swim apart are drawn as one dot. The distance is
// real and worth seeing, so it is text.

import { FEET_PER_MILE, METERS_PER_FOOT, type UnitSystem } from "@/lib/units";

// A position, in the same two field names everything else in the app uses.
export interface GeoPoint {
  latitude: number;
  longitude: number;
}

// Mean Earth radius (IUGG), in metres. A sphere, not the WGS-84 ellipsoid:
// the error is a few tenths of a percent, which is nothing beside the tens of
// metres a dive computer's own fix is out by after a swim under water.
const EARTH_RADIUS_M = 6371008.8;

const toRadians = (degrees: number) => (degrees * Math.PI) / 180;

/**
 * Great-circle distance between two positions, in metres.
 *
 * Haversine, which needs no special case for the antimeridian: it works on the
 * *difference* between the longitudes, so 179.9999°E to 179.9999°W is the 22 m
 * it looks like on a globe rather than the 40,000 km the two numbers suggest.
 */
export function haversineMeters(a: GeoPoint, b: GeoPoint): number {
  const lat1 = toRadians(a.latitude);
  const lat2 = toRadians(b.latitude);
  const deltaLat = lat2 - lat1;
  const deltaLon = toRadians(b.longitude - a.longitude);

  const h =
    Math.sin(deltaLat / 2) ** 2 +
    Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLon / 2) ** 2;

  return 2 * EARTH_RADIUS_M * Math.asin(Math.min(1, Math.sqrt(h)));
}

/**
 * A distance for display: whole metres up to a kilometre, then one decimal -
 * or whole feet up to a mile, then one decimal, for a diver reading in imperial.
 *
 * No decimals on the small unit, deliberately - a consumer GPS fix is good to
 * something like five metres, and "212.4 m" claims a precision the reading
 * never had. Past the big unit's threshold the tenth is back, because "1 km" and
 * "1.9 km" are a real difference to anyone reading where their drift took them,
 * and from a hundred of the big unit it goes again: a site "2345.0 km away" is
 * the finder claiming a precision about a distance nobody swims.
 *
 * Lives here rather than in `lib/units.ts` with the other formatters: the
 * kilometre threshold and the tests behind it are this module's.
 */
export function formatDistance(meters: number, units: UnitSystem): string {
  if (units === "imperial") {
    const feet = meters / METERS_PER_FOOT;
    const wholeFeet = Math.round(feet);
    if (wholeFeet < FEET_PER_MILE) return `${wholeFeet} ft`;
    return bigUnit(feet / FEET_PER_MILE, "mi");
  }

  // Rounded before the comparison, not after: 999.6 m is a thousand metres once
  // the decimals are gone, and "1000 m" beside a "1.0 km" a millimetre further
  // on would look like two different units for the same distance.
  const wholeMeters = Math.round(meters);
  if (wholeMeters < 1000) return `${wholeMeters} m`;
  return bigUnit(meters / 1000, "km");
}

const WHOLE_FROM = 100;

function bigUnit(value: number, unit: string): string {
  const tenths = value.toFixed(1);
  return Number(tenths) < WHOLE_FROM
    ? `${tenths} ${unit}`
    : `${Math.round(value)} ${unit}`;
}