89 lines
3.2 KiB
TypeScript
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="道路地图" />;
|
|
}
|