mirror of
https://github.com/BigBodyCobain/Shadowbroker.git
synced 2026-08-13 22:20:37 +02:00
Size the LOCATE camera from typed precision and place extent
This commit is contained in:
@@ -195,6 +195,21 @@ describe('page.tsx decomposition — no admin-session/proxy regression', () => {
|
||||
expect(locateBar).not.toContain('nominatim.openstreetmap.org');
|
||||
});
|
||||
|
||||
it('LocateBar sizes the camera from typed precision and place extent', () => {
|
||||
const locateBar = readAppFile('LocateBar.tsx');
|
||||
expect(locateBar).toContain('parseCoordinateInput');
|
||||
expect(locateBar).toContain('boundsForCoordinate');
|
||||
expect(locateBar).toContain('sanitizeGeocodeBbox');
|
||||
expect(locateBar).toContain('boundsForPlaceRank');
|
||||
// The extent fields must survive the fetch mapping.
|
||||
expect(locateBar).toContain('place_rank');
|
||||
});
|
||||
|
||||
it('page.tsx forwards LocateBar bounds to the map', () => {
|
||||
const page = readAppFile('page.tsx');
|
||||
expect(page).toMatch(/onLocate=\{\(lat, lng, bounds\)/);
|
||||
});
|
||||
|
||||
it('useRegionDossier uses backend dossier APIs (no browser-direct enrichment)', () => {
|
||||
const hook = fs.readFileSync(
|
||||
path.resolve(__dirname, '../../hooks/useRegionDossier.ts'),
|
||||
|
||||
@@ -3,14 +3,28 @@
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { API_BASE } from '@/lib/api';
|
||||
import { NOMINATIM_DEBOUNCE_MS } from '@/lib/constants';
|
||||
import {
|
||||
parseCoordinateInput,
|
||||
boundsForCoordinate,
|
||||
sanitizeGeocodeBbox,
|
||||
boundsForPlaceRank,
|
||||
type Bounds,
|
||||
} from '@/lib/mapZoom';
|
||||
|
||||
type LocateResult = {
|
||||
label: string;
|
||||
lat: number;
|
||||
lng: number;
|
||||
bounds?: Bounds;
|
||||
};
|
||||
|
||||
/* ── LOCATE BAR ── coordinate / place-name search above bottom status bar ── */
|
||||
export function LocateBar({ onLocate, onOpenChange }: { onLocate: (lat: number, lng: number) => void; onOpenChange?: (open: boolean) => void }) {
|
||||
export function LocateBar({ onLocate, onOpenChange }: { onLocate: (lat: number, lng: number, bounds?: Bounds) => void; onOpenChange?: (open: boolean) => void }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => { onOpenChange?.(open); }, [open]);
|
||||
const [value, setValue] = useState('');
|
||||
const [results, setResults] = useState<{ label: string; lat: number; lng: number }[]>([]);
|
||||
const [results, setResults] = useState<LocateResult[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [searchError, setSearchError] = useState<string | null>(null);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -36,22 +50,17 @@ export function LocateBar({ onLocate, onOpenChange }: { onLocate: (lat: number,
|
||||
return () => document.removeEventListener('mousedown', handler);
|
||||
}, [open]);
|
||||
|
||||
// Parse raw coordinate input: "31.8, 34.8" or "31.8 34.8" or "-12.3, 45.6"
|
||||
const parseCoords = (s: string): { lat: number; lng: number } | null => {
|
||||
const m = s.trim().match(/^([+-]?\d+\.?\d*)[,\s]+([+-]?\d+\.?\d*)$/);
|
||||
if (!m) return null;
|
||||
const lat = parseFloat(m[1]),
|
||||
lng = parseFloat(m[2]);
|
||||
if (lat >= -90 && lat <= 90 && lng >= -180 && lng <= 180) return { lat, lng };
|
||||
return null;
|
||||
};
|
||||
|
||||
const handleSearch = async (q: string) => {
|
||||
setValue(q);
|
||||
// Check for raw coordinates first
|
||||
const coords = parseCoords(q);
|
||||
const coords = parseCoordinateInput(q);
|
||||
if (coords) {
|
||||
setResults([{ label: `${coords.lat.toFixed(4)}, ${coords.lng.toFixed(4)}`, ...coords }]);
|
||||
setResults([{
|
||||
label: `${coords.lat.toFixed(4)}, ${coords.lng.toFixed(4)}`,
|
||||
lat: coords.lat,
|
||||
lng: coords.lng,
|
||||
bounds: boundsForCoordinate(coords.lat, coords.lng, coords.decimals),
|
||||
}]);
|
||||
return;
|
||||
}
|
||||
// Geocode with Nominatim (debounced)
|
||||
@@ -74,11 +83,20 @@ export function LocateBar({ onLocate, onOpenChange }: { onLocate: (lat: number,
|
||||
);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const mapped = (data?.results || []).map(
|
||||
(r: { label: string; lat: number; lng: number }) => ({
|
||||
const mapped: LocateResult[] = (data?.results || []).map(
|
||||
(r: {
|
||||
label: string; lat: number; lng: number;
|
||||
bbox?: string[]; place_rank?: number;
|
||||
}) => ({
|
||||
label: r.label,
|
||||
lat: r.lat,
|
||||
lng: r.lng,
|
||||
// A usable bbox frames the real thing; otherwise fall back to
|
||||
// the rank's typical extent, which is the only honest answer
|
||||
// for Tokyo prefecture, France and bare OSM nodes.
|
||||
bounds:
|
||||
sanitizeGeocodeBbox(r.bbox, r.lat, r.lng) ??
|
||||
boundsForPlaceRank(r.lat, r.lng, r.place_rank),
|
||||
}),
|
||||
);
|
||||
setResults(mapped);
|
||||
@@ -102,8 +120,8 @@ export function LocateBar({ onLocate, onOpenChange }: { onLocate: (lat: number,
|
||||
}, NOMINATIM_DEBOUNCE_MS);
|
||||
};
|
||||
|
||||
const handleSelect = (r: { lat: number; lng: number }) => {
|
||||
onLocate(r.lat, r.lng);
|
||||
const handleSelect = (r: LocateResult) => {
|
||||
onLocate(r.lat, r.lng, r.bounds);
|
||||
setOpen(false);
|
||||
setValue('');
|
||||
setResults([]);
|
||||
@@ -203,7 +221,7 @@ export function LocateBar({ onLocate, onOpenChange }: { onLocate: (lat: number,
|
||||
<div className="absolute bottom-full left-0 right-0 mb-1 bg-[var(--bg-secondary)] border border-[var(--border-primary)] overflow-hidden shadow-[0_-8px_30px_rgba(0,0,0,0.4)] max-h-[200px] overflow-y-auto styled-scrollbar">
|
||||
{results.map((r, i) => (
|
||||
<button
|
||||
key={i}
|
||||
key={`${r.label}-${r.lat}-${r.lng}`}
|
||||
onClick={() => handleSelect(r)}
|
||||
className="w-full text-left px-3 py-2 hover:bg-cyan-950/40 transition-colors border-b border-[var(--border-primary)]/50 last:border-0 flex items-center gap-2"
|
||||
>
|
||||
|
||||
@@ -827,7 +827,7 @@ export default function Dashboard() {
|
||||
>
|
||||
{/* LOCATE BAR — search by coordinates or place name */}
|
||||
<LocateBar
|
||||
onLocate={(lat, lng) => setFlyToLocation({ lat, lng, ts: Date.now() })}
|
||||
onLocate={(lat, lng, bounds) => setFlyToLocation({ lat, lng, bounds, ts: Date.now() })}
|
||||
onOpenChange={setLocateBarOpen}
|
||||
/>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user