refactor(studio): one great circle distance for the map bows and the road fetch

TravelElements ramped how far a leg bows by a local km helper and
roadRoute refused long legs by a second copy of it. Both now call legKm
from legKm.ts, the same formula with the same clamp.
This commit is contained in:
Maurice
2026-10-09 15:01:54 +02:00
parent e6930e00ac
commit 71b7b209dc
4 changed files with 42 additions and 21 deletions
@@ -8,6 +8,7 @@ import {
type LucideIcon,
} from 'lucide-react'
import { fontStack } from './bookFonts'
import { legKm } from './legKm'
import { brightness } from './folioColour'
import { COUNTRY_SHAPES, countryParts, countryWorldPath, projectMercator, unprojectMercator } from './countryShapes'
import { projectOntoTiles, tileView, usableStaticUrl } from './mapTiles'
@@ -358,16 +359,6 @@ function MapView({ el, frameStyle, big = false }: {
*/
const bowed = el.routeArc === 'bow' && trail.length === 0 && points.length > 1
/** Kilometres between two coordinates, for the ramp. */
const km = (a: { lat: number; lng: number }, b: { lat: number; lng: number }) => {
const r = (d: number) => (d * Math.PI) / 180
const dLat = r(b.lat - a.lat)
const dLng = r(b.lng - a.lng)
const h = Math.sin(dLat / 2) ** 2
+ Math.cos(r(a.lat)) * Math.cos(r(b.lat)) * Math.sin(dLng / 2) ** 2
return 12742 * Math.asin(Math.min(1, Math.sqrt(h)))
}
const meanLat = el.points.length
? el.points.reduce((sum, p) => sum + p.lat, 0) / el.points.length
: 0
@@ -389,7 +380,7 @@ function MapView({ el, frameStyle, big = false }: {
const d = Math.hypot(dx, dy)
if (d < 1e-6) return { k: 0, nx: 0, ny: 0 }
const u = Math.min(1, Math.max(0, Math.log(km(a, b) / 150) / Math.log(10)))
const u = Math.min(1, Math.max(0, Math.log(legKm(a, b) / 150) / Math.log(10)))
const ramp = u * u * (3 - 2 * u)
// How much of the leg runs east-west, which is where a bow is truthful.
const eastWest = 0.35 + 0.65 * Math.abs(dx / d)
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest';
import { legKm } from './legKm';
describe('legKm', () => {
it('is zero between a point and itself', () => {
expect(legKm({ lat: 48.1, lng: 11.6 }, { lat: 48.1, lng: 11.6 })).toBe(0);
});
it('measures a degree of latitude as about 111 km', () => {
expect(legKm({ lat: 0, lng: 0 }, { lat: 1, lng: 0 })).toBeCloseTo(111.19, 1);
});
it('is the same both ways', () => {
const a = { lat: 35.68, lng: 139.69 };
const b = { lat: 34.05, lng: -118.24 };
expect(legKm(a, b)).toBeCloseTo(legKm(b, a), 9);
expect(legKm(a, b)).toBeGreaterThan(8700);
expect(legKm(a, b)).toBeLessThan(8900);
});
it('stays defined for antipodal points', () => {
expect(legKm({ lat: 0, lng: 0 }, { lat: 0, lng: 180 })).toBeCloseTo((12742 * Math.PI) / 2, 3);
});
});
+14
View File
@@ -0,0 +1,14 @@
/**
* Kilometres between two coordinates, along the great circle.
*
* Shared by the map element, which ramps how far a leg bows by it, and by the
* road fetch, which refuses to route a leg past its ceiling. The clamp keeps
* `asin` defined when rounding pushes two near antipodal points a hair past 1.
*/
export function legKm(a: { lat: number; lng: number }, b: { lat: number; lng: number }): number {
const r = (d: number) => (d * Math.PI) / 180;
const dLat = r(b.lat - a.lat);
const dLng = r(b.lng - a.lng);
const h = Math.sin(dLat / 2) ** 2 + Math.cos(r(a.lat)) * Math.cos(r(b.lat)) * Math.sin(dLng / 2) ** 2;
return 12742 * Math.asin(Math.min(1, Math.sqrt(h)));
}
+2 -10
View File
@@ -1,4 +1,5 @@
import { calculateRouteWithLegs } from '../Map/RouteCalculator'
import { legKm } from './legKm'
/**
* The roads between a book's stops.
@@ -63,15 +64,6 @@ export interface RoadStop {
lng: number
}
function km(a: RoadStop, b: RoadStop): number {
const r = (d: number) => (d * Math.PI) / 180
const dLat = r(b.lat - a.lat)
const dLng = r(b.lng - a.lng)
const h = Math.sin(dLat / 2) ** 2
+ Math.cos(r(a.lat)) * Math.cos(r(b.lat)) * Math.sin(dLng / 2) ** 2
return 12742 * Math.asin(Math.min(1, Math.sqrt(h)))
}
/**
* Thin a leg to what a printed line can show.
*
@@ -107,7 +99,7 @@ export async function fetchRoads(
const a = stops[i]
const b = stops[i + 1]
if (km(a, b) > ROAD_CEILING_KM) {
if (legKm(a, b) > ROAD_CEILING_KM) {
opts.onProgress?.(i + 1, legs)
continue
}