Fix data pipelines once and for all

This commit is contained in:
Andras Schmelczer 2026-06-10 21:27:32 +01:00
parent 08560476c5
commit 4012e4e047
46 changed files with 4508 additions and 855 deletions

View file

@ -0,0 +1,35 @@
import { describe, expect, it } from 'vitest';
import { MAP_MIN_ZOOM } from './consts';
import { boundsToCenterZoom } from './fit-bounds';
describe('boundsToCenterZoom', () => {
it('centers on the middle of the box', () => {
const target = boundsToCenterZoom({ south: 51.4, north: 51.6, west: -0.3, east: 0.1 });
expect(target.lat).toBeCloseTo(51.5, 5);
expect(target.lng).toBeCloseTo(-0.1, 5);
});
it('zooms close for a small box and far out for a country-sized box', () => {
const street = boundsToCenterZoom({ south: 51.5, north: 51.51, west: -0.11, east: -0.1 });
const england = boundsToCenterZoom({ south: 50.0, north: 55.5, west: -5.7, east: 1.8 });
expect(street.zoom).toBeGreaterThan(england.zoom);
expect(england.zoom).toBeGreaterThanOrEqual(MAP_MIN_ZOOM);
// Greater London-ish box should land in a sensible city-scale zoom range
const london = boundsToCenterZoom({ south: 51.44, north: 51.59, west: -0.31, east: 0.05 });
expect(london.zoom).toBeGreaterThan(8);
expect(london.zoom).toBeLessThan(12);
});
it('caps zoom-in for degenerate (single point) boxes', () => {
const point = boundsToCenterZoom({ south: 51.5, north: 51.5, west: -0.1, east: -0.1 });
expect(point.zoom).toBeLessThanOrEqual(13);
});
it('tolerates swapped corners', () => {
const target = boundsToCenterZoom({ south: 51.6, north: 51.4, west: 0.1, east: -0.3 });
expect(target.lat).toBeCloseTo(51.5, 5);
expect(target.lng).toBeCloseTo(-0.1, 5);
expect(Number.isFinite(target.zoom)).toBe(true);
});
});

View file

@ -0,0 +1,45 @@
import { MAP_MIN_ZOOM } from './consts';
export interface GeoBounds {
south: number;
west: number;
north: number;
east: number;
}
/**
* Nominal viewport used to derive a zoom from a bounding box. The map only
* exposes flyTo(lat, lng, zoom), so we approximate fitBounds; being half a
* zoom level off for unusual window sizes is fine for "show me the matches".
*/
const NOMINAL_VIEWPORT = { width: 1000, height: 700 };
const TILE_SIZE = 512;
/** Keep matches comfortably inside the viewport edges. */
const ZOOM_PADDING = 0.4;
const MAX_FIT_ZOOM = 13;
function mercatorY(lat: number): number {
const rad = (lat * Math.PI) / 180;
return Math.log(Math.tan(Math.PI / 4 + rad / 2));
}
/** Convert a bounding box into a flyTo target that roughly fits it on screen. */
export function boundsToCenterZoom(bounds: GeoBounds): { lat: number; lng: number; zoom: number } {
const south = Math.min(bounds.south, bounds.north);
const north = Math.max(bounds.south, bounds.north);
const west = Math.min(bounds.west, bounds.east);
const east = Math.max(bounds.west, bounds.east);
const lonSpan = Math.max(east - west, 1e-6);
const mercSpan = Math.max(mercatorY(north) - mercatorY(south), 1e-6);
const zoomX = Math.log2((NOMINAL_VIEWPORT.width * 360) / (TILE_SIZE * lonSpan));
const zoomY = Math.log2((NOMINAL_VIEWPORT.height * 2 * Math.PI) / (TILE_SIZE * mercSpan));
const zoom = Math.max(MAP_MIN_ZOOM, Math.min(MAX_FIT_ZOOM, Math.min(zoomX, zoomY) - ZOOM_PADDING));
return {
lat: (south + north) / 2,
lng: (west + east) / 2,
zoom,
};
}