Files
road-compiler/workbench/client/src/components/MapCanvas.tsx

89 lines
3.2 KiB
TypeScript

import { useEffect, useRef } from 'react';
import Map from 'ol/Map';
import View from 'ol/View';
import Select from 'ol/interaction/Select';
import { click } from 'ol/events/condition';
import type { Road, WorkbenchState } from '../types/state';
import { createLayers, updateLayers, type LayerName } from '../map/layers';
interface Props {
state: WorkbenchState;
selected: Road | null;
visible: Partial<Record<LayerName, boolean>>;
scene: boolean;
onSelectRoad: (road: Road) => void;
onFeature: (properties: Record<string, unknown>) => void;
}
export function MapCanvas({ state, selected, visible, scene, onSelectRoad, onFeature }: Props) {
const target = useRef<HTMLDivElement>(null);
const mapRef = useRef<Map | null>(null);
const layersRef = useRef<ReturnType<typeof createLayers> | null>(null);
const selectedRef = useRef<Road | null>(selected);
const stateRef = useRef(state);
selectedRef.current = selected;
stateRef.current = state;
useEffect(() => {
if (!target.current) return;
const layers = createLayers(
() => selectedRef.current,
() => scene,
);
layersRef.current = layers;
const map = new Map({
target: target.current,
layers: Object.values(layers),
view: new View({ center: [0, 0], zoom: 2 }),
});
mapRef.current = map;
const select = new Select({
condition: click,
layers: (layer) => layer !== layers.selectedRoad,
hitTolerance: 12,
style: null,
});
map.addInteraction(select);
const listener = ({ selected: values }: { selected: import('ol/Feature').default[] }) => {
const properties = values[0]?.getProperties();
if (!properties) return;
const roadId =
properties.road_id ||
properties.from_road_id ||
(typeof properties.from_lane_id === 'string'
? properties.from_lane_id.slice(5, properties.from_lane_id.lastIndexOf(':'))
: undefined);
const road = stateRef.current.compiled.model.roads.find((item) => item.id === roadId);
if (road) onSelectRoad(road);
onFeature(properties);
};
select.on('select', listener);
return () => {
select.un('select', listener);
map.setTarget(undefined);
};
// Map construction must be once per mounted canvas; state changes update sources below.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
const layers = layersRef.current;
const map = mapRef.current;
if (!layers || !map) return;
updateLayers(layers, state, selected);
const extent = layers.osm.getSource()!.getExtent();
if (extent && Number.isFinite(extent[0])) map.getView().fit(extent, { padding: [48, 48, 48, 48], maxZoom: 19 });
}, [state, selected]);
useEffect(() => {
const layers = layersRef.current;
if (!layers) return;
Object.entries(visible).forEach(([name, enabled]) =>
layers[name as LayerName]?.setVisible(
!scene || !['osm', 'lanes', 'reference', 'gaodeReference', 'connectors'].includes(name)
? enabled !== false
: false,
),
);
layers.native.changed();
layers.sidewalks.changed();
}, [visible, scene]);
return <div className="map" ref={target} aria-label="道路地图" />;
}