Fly the map to a bounding box when one is supplied

This commit is contained in:
Ethan Morchy
2026-08-11 00:40:48 -07:00
parent 664f218a6f
commit 474cf89141
4 changed files with 52 additions and 8 deletions
@@ -0,0 +1,26 @@
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'fs';
import { join } from 'path';
const viewer = readFileSync(
join(process.cwd(), 'src/components/MaplibreViewer.tsx'),
'utf8',
);
describe('MaplibreViewer bounds camera', () => {
it('derives zoom from bounds via cameraForBounds', () => {
expect(viewer).toContain('cameraForBounds');
});
it('caps the fit, because a tiny bounds otherwise flies to maxZoom 22', () => {
expect(viewer).toMatch(/maxZoom:\s*ZOOM_MAX/);
});
it('handles cameraForBounds returning undefined on oversized padding', () => {
expect(viewer).toContain('ZOOM_FALLBACK');
});
it('leaves the agent fly-to path untouched', () => {
expect(viewer).toContain('zoom: flyToLocation.zoom ?? 8,');
});
});
+1
View File
@@ -437,6 +437,7 @@ export default function Dashboard() {
lat: number;
lng: number;
zoom?: number;
bounds?: [number, number, number, number];
ts: number;
} | null>(null);
+24 -7
View File
@@ -1,6 +1,7 @@
'use client';
import { API_BASE } from '@/lib/api';
import { clampZoom, ZOOM_MAX, ZOOM_FALLBACK } from '@/lib/mapZoom';
import React, { useMemo, useState, useEffect, useCallback, useRef } from 'react';
import Map, {
Source,
@@ -751,16 +752,32 @@ const MaplibreViewer = ({
}, [selectedEntity]);
useEffect(() => {
if (flyToLocation && mapRef.current) {
// Agent moves (sar_focus_aoi) carry their own zoom: a world-view revert
// asks for ~2, a tight AOI for ~9. Ignoring it pinned every move at 8,
// which turned "show the whole world" into a patch of empty ocean.
mapRef.current.flyTo({
center: [flyToLocation.lng, flyToLocation.lat],
zoom: flyToLocation.zoom ?? 8,
if (!flyToLocation || !mapRef.current) return;
const map = mapRef.current.getMap();
if (flyToLocation.bounds) {
// cameraForBounds returns undefined when padding exceeds the viewport,
// and silently returns the map's maxZoom (22) for a degenerate box —
// hence both the clamped padding and the explicit maxZoom.
const { clientWidth, clientHeight } = map.getContainer();
const padding = Math.min(64, Math.min(clientWidth, clientHeight) / 6);
const cam = map.cameraForBounds(flyToLocation.bounds, { padding, maxZoom: ZOOM_MAX });
map.flyTo({
center: cam?.center ?? [flyToLocation.lng, flyToLocation.lat],
zoom: cam ? clampZoom(cam.zoom ?? ZOOM_FALLBACK) : ZOOM_FALLBACK,
duration: 1500,
});
return;
}
// Agent moves (sar_focus_aoi) carry their own zoom: a world-view revert
// asks for ~2, a tight AOI for ~9. Ignoring it pinned every move at 8,
// which turned "show the whole world" into a patch of empty ocean.
mapRef.current.flyTo({
center: [flyToLocation.lng, flyToLocation.lat],
zoom: flyToLocation.zoom ?? 8,
duration: 1500,
});
}, [flyToLocation]);
const earthquakesGeoJSON = useMemo(
+1 -1
View File
@@ -1334,7 +1334,7 @@ export interface MaplibreViewerProps {
activeFilters?: Record<string, string[]>;
effects?: MapEffects;
onEntityClick: (entity: SelectedEntity | null) => void;
flyToLocation: { lat: number; lng: number; zoom?: number; ts?: number } | null;
flyToLocation: { lat: number; lng: number; zoom?: number; bounds?: [number, number, number, number]; ts?: number } | null;
selectedEntity: SelectedEntity | null;
onMouseCoords: (coords: { lat: number; lng: number }) => void;
onRightClick: (coords: { lat: number; lng: number }) => void;