Fix data pipelines once and for all
This commit is contained in:
parent
08560476c5
commit
4012e4e047
46 changed files with 4508 additions and 855 deletions
35
frontend/src/lib/fit-bounds.test.ts
Normal file
35
frontend/src/lib/fit-bounds.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
45
frontend/src/lib/fit-bounds.ts
Normal file
45
frontend/src/lib/fit-bounds.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue