Files
trackgo-fe/packages/ui/src/components/map/location-map.tsx
T
shancheas 6b012a6aae refactor: improve code formatting and organization across multiple components
- Enhanced readability by restructuring code formatting in various components, including API documentation, user management, and sales modules.
- Standardized table and object definitions for better clarity in API documentation.
- Improved layout and indentation in React components for better maintainability.
- Updated unit tests to reflect changes in formatting and ensure consistency.

These changes enhance the overall code quality and maintainability of the project, making it easier for developers to navigate and understand the codebase.
2026-08-27 13:09:57 +07:00

152 lines
4.1 KiB
TypeScript

import { useCallback, useEffect, useMemo, useRef, type RefObject } from 'react';
import { CircleMarker, MapContainer, TileLayer, useMap, useMapEvents } from 'react-leaflet';
import { Box, Text } from '@mantine/core';
import { OSM_ATTRIBUTION, OSM_TILE_URL } from './osm';
import {
DEFAULT_MAP_CENTER,
DEFAULT_MAP_ZOOM,
locationFromLatLng,
SELECTED_MAP_ZOOM,
toLocationLatLng,
} from './location-point';
import 'leaflet/dist/leaflet.css';
import './leaflet-stacking.css';
export interface LocationMapProps {
latitude?: unknown;
longitude?: unknown;
onChange?: (point: { latitude: number; longitude: number }) => void;
height?: number;
emptyLabel?: string;
helperLabel?: string;
defaultCenter?: [number, number];
defaultZoom?: number;
tileUrl?: string;
attribution?: string;
}
function SyncMapView({
latitude,
longitude,
lastPickedRef,
}: {
latitude: unknown;
longitude: unknown;
lastPickedRef: RefObject<{ latitude: number; longitude: number } | null>;
}) {
const map = useMap();
const point = toLocationLatLng(latitude, longitude);
const lat = point?.[0] ?? null;
const lng = point?.[1] ?? null;
const hasCentered = useRef(false);
useEffect(() => {
const id = window.setTimeout(() => map.invalidateSize(), 0);
return () => window.clearTimeout(id);
}, [map]);
useEffect(() => {
if (lat === null || lng === null) return;
const picked = lastPickedRef.current;
if (picked && picked.latitude === lat && picked.longitude === lng) {
return;
}
if (!hasCentered.current) {
hasCentered.current = true;
map.setView([lat, lng], SELECTED_MAP_ZOOM);
return;
}
map.panTo([lat, lng]);
}, [map, lat, lng, lastPickedRef]);
return null;
}
function MapClickPicker({ onPick }: { onPick: (lat: number, lng: number) => void }) {
const onPickRef = useRef(onPick);
onPickRef.current = onPick;
const handlers = useMemo(
() => ({
click(event: { latlng: { lat: number; lng: number } }) {
onPickRef.current(event.latlng.lat, event.latlng.lng);
},
}),
[],
);
useMapEvents(handlers);
return null;
}
export function LocationMap({
latitude,
longitude,
onChange,
height = 280,
emptyLabel = 'No location',
helperLabel,
defaultCenter = DEFAULT_MAP_CENTER,
defaultZoom = DEFAULT_MAP_ZOOM,
tileUrl = OSM_TILE_URL,
attribution = OSM_ATTRIBUTION,
}: LocationMapProps) {
const interactive = Boolean(onChange);
const position = toLocationLatLng(latitude, longitude);
const lastPickedRef = useRef<{ latitude: number; longitude: number } | null>(null);
const center = position ?? defaultCenter;
const zoom = position ? SELECTED_MAP_ZOOM : defaultZoom;
const handlePick = useCallback(
(lat: number, lng: number) => {
const point = locationFromLatLng(lat, lng);
lastPickedRef.current = point;
onChange?.(point);
},
[onChange],
);
if (!interactive && !position) {
return (
<Box h={height} bdrs="md" bd="1px solid var(--mantine-color-default-border)" p="md">
<Text size="sm" c="dimmed">
{emptyLabel}
</Text>
</Box>
);
}
return (
<Box>
<Box
h={height}
bdrs="md"
bd="1px solid var(--mantine-color-default-border)"
className="tg-map-viewport"
style={{ cursor: interactive ? 'crosshair' : undefined }}
>
<MapContainer
center={center}
zoom={zoom}
style={{ height: '100%', width: '100%' }}
scrollWheelZoom={interactive}
>
<TileLayer attribution={attribution} url={tileUrl} />
{position ? (
<CircleMarker
center={position}
radius={10}
pathOptions={{ color: 'var(--mantine-color-blue-8)', fillOpacity: 0.9 }}
/>
) : null}
<SyncMapView latitude={latitude} longitude={longitude} lastPickedRef={lastPickedRef} />
{onChange ? <MapClickPicker onPick={handlePick} /> : null}
</MapContainer>
</Box>
{interactive && helperLabel ? (
<Text size="xs" c="dimmed" mt="xs">
{helperLabel}
</Text>
) : null}
</Box>
);
}