import { useEffect, useState } from 'react'; import { Download, RefreshCw, Save, Upload } from 'lucide-react'; import { api } from './lib/api'; import { MapCanvas } from './components/MapCanvas'; import { Button } from './ui/button'; import type { GeoFeature, Override, Road, WorkbenchState } from './types/state'; import type { LayerName } from './map/layers'; const layerLabels: Array<[LayerName, string]> = [ ['osm', 'OSM 道路中心线'], ['native', '自研道路与路口面'], ['sidewalks', '路缘与步行带'], ['lanes', '车道与转向路径'], ['directionArrows', '道路方向箭头'], ['markings', '车道分隔线与路口转向箭头'], ['centerLines', '道路中心线'], ['edgeLines', '道路外缘线'], ['controls', '斑马线与停止线'], ['signals', '红绿灯设施'], ['gaodeReference', '高德规整路口参考'], ['reference', 'osm2streets 参考面'], ]; const defaultLayers = Object.fromEntries( layerLabels.map(([key]) => [key, key !== 'edgeLines' && key !== 'reference']), ) as Record; const direction = (road: Road) => (road.direction === 'forward' ? '沿 OSM 方向' : '逆 OSM 方向'); const label = (road: Road) => road.tags.name || `${road.highway}(OSM ${road.osmWayIds.join(', ')})`; const laneIndex = (id: string) => Number(id.split(':').at(-1)); function App() { const [state, setState] = useState(); const [selected, setSelected] = useState(null); const [staged, setStaged] = useState([]); const [layers, setLayers] = useState(defaultLayers); const [scene, setScene] = useState(false); const [status, setStatus] = useState('正在加载工作台...'); const [filter, setFilter] = useState<'all' | 'candidates' | 'other'>('all'); const [marking, setMarking] = useState | null>(null); useEffect(() => { api .state() .then((value) => { if ('compiled' in value) { setState(value); setStatus(`已加载 ${value.compiled.model.roads.length} 条方向道路`); } else setStatus('请导入 OSM 文件以开始'); }) .catch((error: Error) => setStatus(error.message)); }, []); const stage = (change: Override) => setStaged((current) => [...current.filter((item) => item.id !== change.id), change]); const save = async () => { if (!state || !staged.length) return true; try { const overrides = [ ...state.overrides.overrides.filter((item) => !staged.some((change) => change.id === item.id)), ...staged, ]; const result = await api.overrides(overrides); setState({ ...state, overrides: result.overrides }); setStaged([]); setStatus('已保存,点击“保存并重新生成”写入几何'); return true; } catch (error) { setStatus((error as Error).message); return false; } }; const compile = async () => { if (!(await save())) return; try { setStatus('正在重新生成...'); const next = await api.compile(); setState(next); setSelected((current) => next.compiled.model.roads.find((road) => road.id === current?.id) || null); setStatus('已保存并重新生成'); } catch (error) { setStatus((error as Error).message); } }; if (!state) return ( { setState(next); setStatus(`已导入 ${next.areaId}`); }} /> ); const diagnostics = state.compiled.diagnostics.filter( (item) => item.rule !== 'ordinary-junction-surface' && (filter === 'all' || filter === 'candidates' ? Boolean(item.manualCandidates?.length) : !item.manualCandidates?.length), ); const handleFeature = (properties: Record) => { setMarking(properties); const uid = properties.signal_uid; if (typeof uid === 'string') setStatus(`已选中原生红绿灯 ${uid}`); else if (properties.effective_style) setStatus('已选中标线,可在右侧暂存样式'); }; return ( <>
道路编译工作台 {state.areaId} {status} {staged.length ? `未保存修改 ${staged.length} 项` : '所有修改已保存'} 导出道路包
); } function ImportScreen({ status, onImported }: { status: string; onImported: (state: WorkbenchState) => void }) { const [file, setFile] = useState(); const [busy, setBusy] = useState(false); return (
{ event.preventDefault(); if (!file) return; setBusy(true); try { onImported(await api.import(file)); } catch (error) { console.error(error); } finally { setBusy(false); } }} >

导入 OSM 地图

{status}

setFile(event.target.files?.[0])} required />
); } function Inspector({ road, state, staged, marking, stage, status, setState, }: { road: Road | null; state: WorkbenchState; staged: Override[]; marking: Record | null; stage: (item: Override) => void; status: (value: string) => void; setState: (next: WorkbenchState) => void; }) { const [width, setWidth] = useState(0); const [lanes, setLanes] = useState(1); const [left, setLeft] = useState(false); const [right, setRight] = useState(false); useEffect(() => { if (road) { setWidth(road.widthMeters); setLanes(road.laneCount); setLeft(road.sidewalkLeft); setRight(road.sidewalkRight); } }, [road]); const endpoint = state.compiled.model.endpoints.find((item) => item.roadId === road?.id && item.side === 'end'); const movements = state.compiled.movements?.filter((item) => item.fromRoadId === road?.id) || []; const effective = (id: string, fallback: boolean) => ((staged.find((item) => item.id === id) || state.overrides.overrides.find((item) => item.id === id))?.enabled as boolean | undefined) ?? fallback; const updateSignal = async (features: GeoFeature[]) => { try { const result = await api.signals({ ...state.trafficSignals, assemblies: { ...state.trafficSignals.assemblies, features }, }); setState({ ...state, trafficSignals: result.trafficSignals, trafficRuntime: result.trafficRuntime }); status('红绿灯已保存'); } catch (error) { status((error as Error).message); } }; const signalUid = typeof marking?.signal_uid === 'string' ? marking.signal_uid : ''; const signal = state.trafficSignals.assemblies.features.find((item) => item.properties?.signal_uid === signalUid); return ( ); } function MarkingEditor({ marking, stage, }: { marking: Record | null; stage: (item: Override) => void; }) { const style = typeof marking?.effective_style === 'string' ? marking.effective_style : ''; const [value, setValue] = useState(style || 'yellow-dashed'); useEffect(() => setValue(style || 'yellow-dashed'), [style]); if (!style || !marking) return null; const roadId = String(marking.road_id); const segment = marking.segment_id; const kind = String(marking.provenance || ''); const save = () => { const [color, pattern] = value.replace('double-', '').split('-'); if (segment) stage({ id: `道路中心线:${segment}`, kind: 'center-line-style', segmentId: segment, color, pattern, double: value.startsWith('double-'), }); else if (kind.includes('edge-line')) stage({ id: `道路外缘线:${roadId}:${marking.side}`, kind: 'edge-line-style', roadId, side: marking.side, color, pattern, }); else stage({ id: `车道分隔线:${roadId}:${marking.left_lane_index}-${marking.right_lane_index}`, kind: 'lane-separator-style', roadId, leftLaneIndex: marking.left_lane_index, rightLaneIndex: marking.right_lane_index, color, pattern, }); }; return (
{ event.preventDefault(); save(); }} >

标线样式

); } function SignalEditor({ state, signal, status, update, }: { state: WorkbenchState; signal?: GeoFeature; status: (text: string) => void; update: (features: GeoFeature[]) => Promise; }) { const [selected, setSelected] = useState(''); useEffect(() => setSelected(String(signal?.properties?.signal_uid || '')), [signal]); const feature = state.trafficSignals.assemblies.features.find((item) => item.properties?.signal_uid === selected); const saveField = (key: string, value: unknown) => { if (!feature) return; void update( state.trafficSignals.assemblies.features.map((item) => item === feature ? { ...item, properties: { ...item.properties, [key]: value } } : item, ), ); }; return (

原生红绿灯

{feature ? ( <> ) : null}
); } function Candidates({ state, setState, status, }: { state: WorkbenchState; setState: (state: WorkbenchState) => void; status: (text: string) => void; }) { const candidates = state.debug?.junctionCandidates || []; if (!candidates.length) return null; return (

复杂路口候选(debug)

{candidates.map((candidate) => ( ))}
); } export default App;