@@ -1,171 +1,103 @@
"use strict ";
import Map from "/vendor/ol/Map.js ";
import View from "/vendor/ol/View.js" ;
import VectorLayer from "/vendor/ol/layer/Vector.js" ;
import VectorSource from "/vendor/ol/source/Vector.js" ;
import GeoJSON from "/vendor/ol/format/GeoJSON.js" ;
import Feature from "/vendor/ol/Feature.js" ;
import LineString from "/vendor/ol/geom/LineString.js" ;
import Style from "/vendor/ol/style/Style.js" ;
import Fill from "/vendor/ol/style/Fill.js" ;
import Stroke from "/vendor/ol/style/Stroke.js" ;
import CircleStyle from "/vendor/ol/style/Circle.js" ;
import Select from "/vendor/ol/interaction/Select.js" ;
import { click } from "/vendor/ol/events/condition.js" ;
const canvas = document . querySelector ( "#map" ) ;
const context = canvas . getContext ( "2d " ) ;
const geojson = new GeoJSON ( ) ;
const areaLabel = document . querySelector ( "#area " ) ;
const status = document . querySelector ( "#status" ) ;
const form = document . querySelector ( "#road-form" ) ;
const areaLabel = document . querySelector ( "#area " ) ;
const hint = document . querySelector ( "#hint " ) ;
const roadName = document . querySelector ( "#road-name" ) ;
const movementSummary = document . querySelector ( "#movement-summary" ) ;
const directionSwitch = document . querySelector ( "#direction-switch" ) ;
const widthInput = document . querySelector ( "#width" ) ;
const lanesInput = document . querySelector ( "#lanes" ) ;
const leftInput = document . querySelector ( "#left" ) ;
const rightInput = document . querySelector ( "#right" ) ;
const evidence = document . querySelector ( "#evidence" ) ;
const diagnostics = document . querySelector ( "#diagnostics" ) ;
const connectionsBox = document . querySelector ( "#connections" ) ;
const addConnectionButton = document . querySelector ( "#add-connection" ) ;
const saveButton = document . querySelector ( "#save" ) ;
const compileButton = document . querySelector ( "#compile" ) ;
const diagnostics = document . querySelector ( "#diagnostics" ) ;
const directionSwitch = document . querySelector ( "#direction-switch" ) ;
const addConnectionButton = document . querySelector ( "#add-connection" ) ;
const movementSummary = document . querySelector ( "#movement-summary" ) ;
let state ;
let selected = null ;
let selectedRoad = null ;
let staged = [ ] ;
const source = ( ) => new VectorSource ( ) ;
const layers = {
reference : new VectorLayer ( { source : source ( ) , style : new Style ( { fill : new Fill ( { color : "rgba(123, 140, 148, .28)" } ) , stroke : new Stroke ( { color : "#8999a0" , width : 1 } ) } ) } ) ,
native : new VectorLayer ( { source : source ( ) , style : ( feature ) => feature . get ( "native_id" ) ? . startsWith ( "junction:" ) ? new Style ( { fill : new Fill ( { color : "rgba(12, 116, 91, .38)" } ) , stroke : new Stroke ( { color : "#0e785f" , width : 1.5 } ) } ) : new Style ( { fill : new Fill ( { color : "rgba(40, 105, 86, .35)" } ) , stroke : new Stroke ( { color : "#296956" , width : 1 } ) } ) } ) ,
osm : new VectorLayer ( { source : source ( ) , style : ( feature ) => new Style ( { stroke : new Stroke ( { color : feature . get ( "road_id" ) === selectedRoad ? . id ? "#006e91" : "#263630" , width : feature . get ( "road_id" ) === selectedRoad ? . id ? 5 : 2 } ) } ) } ) ,
lanes : new VectorLayer ( { source : source ( ) , style : ( feature ) => new Style ( { stroke : new Stroke ( { color : feature . get ( "road_id" ) === selectedRoad ? . id ? "#006e91" : "#f5f6ee" , width : feature . get ( "road_id" ) === selectedRoad ? . id ? 3 : 1.3 , lineDash : [ 5 , 4 ] } ) } ) } ) ,
connectors : new VectorLayer ( { source : source ( ) , style : ( feature ) => effectiveConnectorEnabled ( feature . getProperties ( ) ) ? new Style ( { stroke : new Stroke ( { color : roadIdFromLane ( feature . get ( "from_lane_id" ) ) === selectedRoad ? . id ? "#d1226f" : "#ad3a76" , width : roadIdFromLane ( feature . get ( "from_lane_id" ) ) === selectedRoad ? . id ? 4 : 2 , lineDash : [ 7 , 5 ] } ) } ) : null } ) ,
diagnostics : new VectorLayer ( { source : source ( ) , style : ( feature ) => new Style ( { image : new CircleStyle ( { radius : 6 , fill : new Fill ( { color : feature . get ( "severity" ) === "error" ? "#bf3b2e" : "#d49318" } ) , stroke : new Stroke ( { color : "#fff" , width : 1 } ) } ) } ) } ) ,
} ;
const map = new Map ( { target : "map" , layers : [ layers . reference , layers . native , layers . osm , layers . lanes , layers . connectors , layers . diagnostics ] , view : new View ( { center : [ 0 , 0 ] , zoom : 2 } ) } ) ;
const select = new Select ( { condition : click , layers : [ layers . osm , layers . lanes , layers . connectors , layers . native ] , hitTolerance : 8 , style : new Style ( { stroke : new Stroke ( { color : "#005e89" , width : 5 } ) , fill : new Fill ( { color : "rgba(0, 94, 137, .18)" } ) } ) } ) ;
map . addInteraction ( select ) ;
select . on ( "select" , ( { selected } ) => { const feature = selected [ 0 ] ; if ( feature ) selectRoad ( roadForFeature ( feature ) ) ; } ) ;
map . on ( "pointermove" , ( event ) => { map . getTargetElement ( ) . style . cursor = map . hasFeatureAtPixel ( event . pixel , { hitTolerance : 8 } ) ? "pointer" : "" ; } ) ;
function message ( text ) { status . textContent = text ; }
function roadLabel ( road ) { return road . tags . name || ` ${ road . highway } ( OSM ${ road . osmWayIds . join ( ", " ) } ) ` ; }
function coord ( point ) { const b = state . bounds ; return [ ( point [ 0 ] - b . minX ) / ( b . maxX - b . minX ) * c anvas . width , canvas . height - ( point [ 1 ] - b . minY ) / ( b . maxY - b . minY ) * canvas . height ] ; }
function setBounds ( ) { const points = state . compiled . model . roads . flatMap ( ( road ) => road . centerline ) ; const xs = points . map ( ( point ) => point [ 0 ] ) ; const ys = points . map ( ( point ) => point [ 1 ] ) ; const pad = Math . max ( ( Math . max ( ... xs ) - Math . min ( ... xs ) ) * 0.06 , 0.0001 ) ; state . bounds = { minX : Math . min ( ... xs ) - pad , maxX : Math . max ( ... xs ) + pad , minY : Math . min ( ... ys ) - pad , maxY : Math . max ( ... ys ) + pad } ; }
function resize ( ) { canvas . width = canvas . clientWidth * devicePixelRatio ; canvas . height = canvas . clientHeight * devicePixelRatio ; draw ( ) ; }
function polygon ( feature , fill ) { const ring = feature . geometry ? . coordinates ? . [ 0 ] ; if ( ! ring ) return ; context . beginPath ( ) ; ring . forEach ( ( point , index ) => { const point2d = coord ( point ) ; index ? context . lineTo ( ... point2d ) : context . moveTo ( ... point2d ) ; } ) ; context . fillStyle = fill ; context . fill ( ) ; }
function layerLine ( feature , stroke , width , dashed ) { const points = feature . geometry ? . coordinates ; if ( ! points ? . length ) return ; context . save ( ) ; context . setLineDash ( dashed ? [ 5 * devicePixelRatio , 4 * devicePixelRatio ] : [ ] ) ; context . beginPath ( ) ; points . forEach ( ( point , index ) => { const point2d = coord ( point ) ; index ? context . lineTo ( ... point2d ) : context . moveTo ( ... point2d ) ; } ) ; context . strokeStyle = stroke ; context . lineWidth = width * devicePixelRatio ; context . stroke ( ) ; context . restore ( ) ; }
function draw ( ) {
if ( ! state ) return ;
context . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
const layers = state . layers || { } ;
for ( const feature of layers . osm2streetsRoadSurface ? . features || [ ] ) polygon ( feature , "#9ba8ae55" ) ;
for ( const feature of layers . nativeRoadSurface ? . features || [ ] ) polygon ( feature , "#28695666" ) ;
for ( const feature of layers . nativeIntersectionSurface ? . features || [ ] ) polygon ( feature , "#0e786066" ) ;
for ( const feature of layers . laneCenterlines ? . features || [ ] ) {
const active = feature . properties . road _id === selected ? . id ;
layerLine ( feature , active ? "#006e91" : "#f5f6ee" , active ? 2.4 : 1.2 , true ) ;
}
for ( const feature of layers . connectors ? . features || [ ] ) {
if ( ! effectiveConnectorEnabled ( feature . properties ) ) continue ;
const active = feature . properties . from _lane _id . startsWith ( ` lane: ${ selected ? . id } : ` ) ;
layerLine ( feature , active ? "#d1226f" : "#ad3a76" , active ? 3 : 1.8 , true ) ;
}
for ( const road of state . compiled . model . roads ) {
context . beginPath ( ) ; road . centerline . forEach ( ( point , index ) => { const point2d = coord ( point ) ; index ? context . lineTo ( ... point2d ) : context . moveTo ( ... point2d ) ; } ) ;
context . strokeStyle = road . id === selected ? . id ? "#006e91" : "#263630" ;
context . lineWidth = ( road . id === selected ? . id ? 4 : 2 ) * devicePixelRatio ;
context . stroke ( ) ;
}
if ( selected ) drawDirectionArrow ( selected ) ;
for ( const diagnostic of state . compiled . diagnostics ) {
if ( ! diagnostic . geometry ) continue ;
const point = coord ( diagnostic . geometry . coordinates ) ; const isJunction = diagnostic . rule === "ordinary-junction-surface" ; context . fillStyle = isJunction ? "#0e7860" : diagnostic . severity === "error" ? "#bf3b2e" : "#d49318" ; context . beginPath ( ) ; context . arc ( ... point , isJunction ? 4 * devicePixelRatio : 5 * devicePixelRatio , 0 , Math . PI * 2 ) ; context . fill ( ) ;
}
if ( state . focusedDiagnostic ? . geometry ) { const point = coord ( state . focusedDiagnostic . geometry . coordinates ) ; context . strokeStyle = "#006e91" ; context . lineWidth = 3 * devicePixelRatio ; context . beginPath ( ) ; context . arc ( ... point , 11 * devicePixelRatio , 0 , Math . PI * 2 ) ; context . stroke ( ) ; }
function roadIdFromLane ( laneId ) { return typeof laneId === "string" ? l aneId . slice ( 5 , laneId . lastIndexOf ( ":" ) ) : "" ; }
function roadForFeature ( feature ) {
const properties = feature . getProperties ( ) ;
const roadId = properties . road _id || roadIdFromLane ( properties . from _lane _id ) || properties . directional _road _ids ? . split ( "," ) [ 0 ] ;
return state . compiled . model . roads . find ( ( road ) => road . id === roadId ) || null ;
}
function drawDirectionArrow ( road ) {
const middle = Math . max ( 1 , Math . floor ( road . centerline . length / 2 ) ) ;
const a = coord ( road . centerline [ middle - 1 ] ) ; const b = coord ( ro ad . centerline [ middl e] ) ;
const angle = Math . atan2 ( b [ 1 ] - a [ 1 ] , b [ 0 ] - a [ 0 ] ) ; const size = 10 * devicePixelRatio ;
context . save ( ) ; context . translate ( b [ 0 ] , b [ 1 ] ) ; context . rotate ( angle ) ; context . fillStyle = "#006e91" ;
context . beginPath ( ) ; context . moveTo ( size , 0 ) ; context . lineTo ( - size * 0.8 , - size * 0.6 ) ; context . li neTo ( - size * 0.8 , size * 0.6 ) ; context . closePath ( ) ; context . fill ( ) ; context . restore ( ) ;
function readFeatures ( collection ) { return geojson . readFeatures ( collection || { type : "FeatureCollection" , features : [ ] } , { dataProjection : "EPSG:4326" , featureProjection : "EPSG:3857" } ) ; }
function rawRoadFeatures ( ) { return state . compiled . model . roads . map ( ( road ) => new Feature ( { geometry : new LineString ( road . centerline ) . transform ( "EPSG:4326" , "EPSG:3857" ) , road _id : road . id } ) ) ; }
function updateSources ( ) {
layers . reference . getSource ( ) . clear ( ) ; layers . reference . getSource ( ) . addFeatures ( re adFeatures ( state . layers . osm2streetsRoadSurfac e) ) ;
layers . native . getSource ( ) . clear ( ) ; layers . native . getSource ( ) . addFeatures ( [ ... readFeatures ( state . layers . nativeRoadSurface ) , ... readFeatures ( state . layers . nativeIntersectionSurface ) ] ) ;
layers . osm . getSource ( ) . clear ( ) ; layers . osm . getSource ( ) . addFeatures ( rawRoadFeatures ( ) ) ;
layers . lanes . getSource ( ) . clear ( ) ; layers . la nes . getSource ( ) . addFeatures ( readFeatures ( state . layers . laneCenterlines ) ) ;
layers . connectors . getSource ( ) . clear ( ) ; layers . connectors . getSource ( ) . addFeatures ( readFeatures ( state . layers . connectors ) ) ;
layers . diagnostics . getSource ( ) . clear ( ) ; layers . diagnostics . getSource ( ) . addFeatures ( readFeatures ( { type : "FeatureCollection" , features : state . compiled . diagnostics . filter ( ( item ) => item . geometry ) . map ( ( { geometry , ... properties } ) => ( { type : "Feature" , properties , geometry } ) ) } ) ) ;
const extent = layers . osm . getSource ( ) . getExtent ( ) ; if ( Number . isFinite ( extent [ 0 ] ) ) map . getView ( ) . fit ( extent , { padding : [ 48 , 48 , 48 , 48 ] , maxZoom : 19 } ) ;
}
function select ( road ) {
selected = road ; form . hidden = ! road ; document . querySelector ( "#hint" ) . hidden = Boolean ( road ) ;
if ( ! road ) return ;
document . querySelector ( "#road-name" ) . textContent = ` ${ roadLabel ( road ) } ( ${ road . direction === "forward" ? "沿 OSM 方向行驶" : "逆 OSM 方向行驶" } ) ` ;
widthInput . value = road . widthMeters ; lanesInput . value = road . laneCount ; leftInput . checked = road . sidewalkLeft ; rightInput . checked = road . sidewalkRight ;
evidence . textContent = JSON . stringify ( { OSM道路 : road . osmWayIds , 名称 : road . tags . name || "未标注" , 参数来源 : road . provenance , 已应用修改 : road . appliedOverrideIds , 原始标签 : road . tags } , null , 2 ) ;
renderMovementSummary ( road ) ;
renderDirectionSwitch ( road ) ;
renderConnections ( road ) ; draw ( ) ;
}
function renderMovementSummary ( road ) {
const connectors = ( state . layers . connectors ? . features || [ ] ) . filter ( ( feature ) => feature . properties . from _lane _id . startsWith ( ` lane: ${ road . id } : ` ) && effectiveConnectorEnabled ( feature . properties ) ) ;
const byTurn = connectors . reduce ( ( result , feature ) => { result [ feature . properties . turn ] = ( result [ feature . properties . turn ] || 0 ) + 1 ; return result ; } , { } ) ;
const labels = { left : "左转" , through : "直行" , right : "右转" , uturn : "掉头" } ;
movementSummary . textContent = connectors . length ? ` 当前方向已生成 ${ connectors . length } 条路径: ${ Object . entries ( byTurn ) . map ( ( [ turn , count ] ) => ` ${ labels [ turn ] || turn } ${ count } ` ) . join ( ", " ) } ` : "当前方向没有已生成的转向路径" ;
}
function renderDirectionSwitch ( road ) {
directionSwitch . innerHTML = "" ;
const alternatives = state . compiled . model . roads . filter ( ( item ) => item . osmWayIds . join ( "," ) === road . osmWayIds . join ( "," ) ) ;
if ( alternatives . length < 2 ) { directionSwitch . textContent = "单向道路:沿 OSM 节点顺序行驶" ; return ; }
const note = document . createElement ( "label" ) ; note . textContent = "编辑方向(地图蓝色箭头表示当前方向)" ; directionSwitch . append ( note ) ;
for ( const item of alternatives ) {
const button = document . createElement ( "button" ) ;
button . type = "button" ; button . textContent = item . direction === "forward" ? "沿 OSM 节点顺序" : "逆 OSM 节点顺序" ;
button . disabled = item . id === road . id ; button . onclick = ( ) => select ( item ) ; directionSwitch . append ( button ) ;
}
}
function renderConnections ( road ) {
const box = document . querySelector ( "#connections" ) ; box . innerHTML = "" ;
const endpoint = state . compiled . model . endpoints . find ( ( item ) => item . roadId === road . id && item . side === "end" ) ;
addConnectionButton . hidden = ! endpoint ;
addConnectionButton . onclick = ( ) => renderAdditionalConnections ( road , endpoint ) ;
const rows = state . compiled . model . connections . filter ( ( connection ) => connection . fromEndpointId === endpoint ? . id ) ;
if ( ! rows . length ) { box . textContent = "当前行驶方向到达道路终点后,没有可编辑的驶出道路。" ; return ; }
const intro = document . createElement ( "p" ) ; intro . textContent = "到达终点路口后,允许驶入:" ; box . append ( intro ) ;
const seen = new Set ( ) ;
for ( const connection of rows ) {
const targetEndpoint = state . compiled . model . endpoints . find ( ( item ) => item . id === connection . toEndpointId ) ;
const target = state . compiled . model . roads . find ( ( item ) => item . id === targetEndpoint ? . roadId ) ;
if ( ! target || seen . has ( target . id ) ) continue ;
seen . add ( target . id ) ;
const input = document . createElement ( "input" ) ; const label = document . createElement ( "label" ) ;
input . type = "checkbox" ; input . checked = effectiveConnectionEnabled ( connection ) ; input . onchange = ( ) => { stageConnection ( connection , input . checked ) ; renderConnections ( road ) ; draw ( ) ; } ;
label . append ( input , ` ${ turnName ( road , target ) } : ${ roadLabel ( target ) } ` ) ; box . append ( label ) ;
renderLaneConnectionControls ( box , connection ) ;
}
}
function renderLaneConnectionControls ( box , connection ) {
const connectors = state . layers . connectors ? . features . filter ( ( feature ) => feature . properties . connection _id === connection . id ) || [ ] ;
const rows = connectors . map ( ( feature ) => ( { from _lane _id : feature . properties . from _lane _id , to _lane _id : feature . properties . to _lane _id , enabled : true } ) ) ;
const sourceRoad = connection . fromEndpointId . slice ( "endpoint:" . length ) . replace ( /:end$/ , "" ) ;
const targetRoad = connection . toEndpointId . slice ( "endpoint:" . length ) . replace ( /:start$/ , "" ) ;
for ( const override of state . overrides . overrides . filter ( ( item ) => item . kind === "lane-connection" && item . fromLaneId . startsWith ( ` lane: ${ sourceRoad } : ` ) && item . toLaneId . startsWith ( ` lane: ${ targetRoad } : ` ) ) ) {
if ( ! rows . some ( ( row ) => row . from _lane _id === override . fromLaneId && row . to _lane _id === override . toLaneId ) ) rows . push ( { from _lane _id : override . fromLaneId , to _lane _id : override . toLaneId , enabled : override . enabled } ) ;
}
for ( const row of rows ) appendLaneConnectionControl ( box , row ) ;
}
function appendLaneConnectionControl ( box , row ) { const fromIndex = row . from _lane _id . split ( ":" ) . at ( - 1 ) ; const toIndex = row . to _lane _id . split ( ":" ) . at ( - 1 ) ; const input = document . createElement ( "input" ) ; const label = document . createElement ( "label" ) ; input . type = "checkbox" ; input . checked = effectiveLaneEnabled ( row ) ; input . onchange = ( ) => { stageLaneConnection ( row , input . checked ) ; if ( selected ) { renderConnections ( selected ) ; renderMovementSummary ( selected ) ; draw ( ) ; } } ; label . append ( input , ` 第 ${ fromIndex } 车道 → 第 ${ toIndex } 车道 ` ) ; box . append ( label ) ; }
function renderAdditionalConnections ( road , endpoint ) {
const box = document . querySelector ( "#connections" ) ;
const existing = new Set ( state . compiled . model . connections . filter ( ( connection ) => connection . fromEndpointId === endpoint . id ) . map ( ( connection ) => connection . toEndpointId ) ) ;
const candidates = state . compiled . model . endpoints . filter ( ( item ) => item . side === "start" && item . roadId !== road . id && ! existing . has ( item . id ) && endpointDistanceMeters ( endpoint , item ) <= 35 ) ;
box . innerHTML = "" ;
if ( ! candidates . length ) { box . textContent = "附近 35 米内没有可新增的驶出道路。请检查 OSM 路口节点,或选择另一条道路。" ; return ; }
const intro = document . createElement ( "p" ) ; intro . textContent = "选择要新增允许驶入的道路:" ; box . append ( intro ) ;
for ( const targetEndpoint of candidates ) {
const target = state . compiled . model . roads . find ( ( item ) => item . id === targetEndpoint . roadId ) ;
const button = document . createElement ( "button" ) ; button . type = "button" ; button . textContent = ` ${ turnName ( road , target ) } : ${ roadLabel ( target ) } (相距 ${ endpointDistanceMeters ( endpoint , targetEndpoint ) . toFixed ( 1 ) } 米) ` ;
button . onclick = ( ) => { stageConnection ( { id : ` manual: ${ endpoint . id } : ${ targetEndpoint . id } ` , fromEndpointId : endpoint . id , toEndpointId : targetEndpoint . id } , true ) ; renderConnections ( road ) ; } ;
box . append ( button ) ;
}
}
function turnName ( from , to ) {
const a = heading ( from . centerline . at ( - 2 ) , from . centerline . at ( - 1 ) ) ;
const b = heading ( to . centerline [ 0 ] , to . centerline [ 1 ] ) ;
const delta = ( ( b - a + 540 ) % 360 ) - 180 ;
if ( Math . abs ( delta ) >= 150 ) return "掉头" ;
if ( Math . abs ( delta ) <= 30 ) return "直行" ;
return delta > 0 ? "右转" : "左转" ;
}
function heading ( a , b ) { return Math . atan2 ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) * 180 / Math . PI ; }
function endpointDistanceMeters ( a , b ) { const dx = ( a . coordinate [ 0 ] - b . coordinate [ 0 ] ) * 111320 * Math . cos ( a . coordinate [ 1 ] * Math . PI / 180 ) ; const dy = ( a . coordinate [ 1 ] - b . coordinate [ 1 ] ) * 111320 ; return Math . hypot ( dx , dy ) ; }
function stageConnection ( connection , enabled ) { const id = ` 连接: ${ connection . id } ` ; staged = staged . filter ( ( item ) => item . id !== id ) ; staged . push ( { id , kind : "junction-connection" , fromEndpointId : connection . fromEndpointId , toEndpointId : connection . toEndpointId , enabled } ) ; message ( "有未保存修改" ) ; }
function stageLaneConnection ( connector , enabled ) { const id = ` 车道连接: ${ connector . from _lane _id } -> ${ connector . to _lane _id } ` ; staged = staged . filter ( ( item ) => item . id !== id ) ; staged . push ( { id , kind : "lane-connection" , fromLaneId : connector . from _lane _id , toLaneId : connector . to _lane _id , enabled } ) ; message ( "有未保存修改" ) ; }
function effectiveLaneEnabled ( connector ) { const id = ` 车道连接: ${ connector . from _lane _id } -> ${ connector . to _lane _id } ` ; const override = [ ... staged , ... state . overrides . overrides ] . find ( ( item ) => item . id === id ) ; return override ? override . enabled : connector . enabled !== false ; }
function effectiveConnectionEnabled ( connection ) { const id = ` 连接: ${ connection . id } ` ; const override = [ ... staged , ... state . overrides . overrides ] . find ( ( item ) => item . id === id ) ; return override ? override . enabled : connection . enabled ; }
function effectiveConnectorEnabled ( connector ) { const connection = state . compiled . model . connections . find ( ( item ) => item . id === connector . connection _id ) ; return effectiveLaneEnabled ( connector ) && ( ! connection || effectiveConnectionEnabled ( connection ) ) ; }
function pointToSegmentDistance ( point , a , b ) { const dx = b [ 0 ] - a [ 0 ] ; const dy = b [ 1 ] - a [ 1 ] ; const lengthSquared = dx * dx + dy * dy ; const t = lengthSquared ? Math . max ( 0 , Math . min ( 1 , ( ( point [ 0 ] - a [ 0 ] ) * dx + ( point [ 1 ] - a [ 1 ] ) * dy ) / lengthSquared ) ) : 0 ; return Math . hypot ( point [ 0 ] - ( a [ 0 ] + t * dx ) , point [ 1 ] - ( a [ 1 ] + t * dy ) ) ; }
function pickRoad ( point ) { let best = null ; let distance = Infinity ; for ( const road of state . compiled . model . roads ) for ( let index = 1 ; index < road . centerline . length ; index += 1 ) { const candidate = pointToSegmentDistance ( point , coord ( road . centerline [ index - 1 ] ) , coord ( road . centerline [ index ] ) ) ; if ( candidate < distance ) { distance = candidate ; best = road ; } } return distance <= 18 * devicePixelRatio ? best : null ; }
canvas . onclick = ( event ) => { const rect = canvas . getBoundingClientRect ( ) ; select ( pickRoad ( [ ( event . clientX - rect . left ) * devicePixelRatio , ( event . clientY - rect . top ) * devicePixelRatio ] ) ) ; } ;
form . onsubmit = ( event ) => { event . preventDefault ( ) ; const id = ` 道路: ${ selected . id } ` ; staged = staged . filter ( ( item ) => item . id !== id ) ; staged . push ( { id , kind : "road" , roadId : selected . id, widthMeters : Number ( widthInput . value ) , laneCount : Number ( lanesInput . value ) , sidewalkLeft : leftInput . checked , sidewalkRight : rightInput . checked } ) ; message ( "有未保存修改" ) ; } ;
saveButton . onclick = async ( ) => { const existing = state . overrides . overrides . filter ( ( item ) => ! staged . some ( ( change ) => change . id === item . id ) ) ; const response = await fetch ( "/api/overrides" , { method : "POST" , headers : { "Content-Type" : "application/json" } , body : JSON . stringify ( { schema : "native-road-overrides/v1" , overrides : [ ... existing , ... staged ] } ) } ) ; const data = await response . json ( ) ; if ( ! data . ok ) return message ( data . error ) ; state . overrides = data . overrides ; staged = [ ] ; message ( "已保存,点击“保存并重新生成”生效" ) ; } ;
compileButton . onclick = async ( ) => { message ( "正在重新生成..." ) ; const response = await fetch ( "/api/compile" , { method : "POST" } ) ; state = await response . json ( ) ; staged = [ ] ; setup ( ) ; message ( "已按保存的修改重新生成" ) ; } ;
function focusDiagnostic ( diagnostic ) { state . focusedDiagnostic = diagnostic ; const road = state . compiled . model . roads . find ( ( item ) => item . id === diagnostic . subjectId ) ; if ( road ) { select ( road ) ; message ( diagnostic . message ) ; } else { draw ( ) ; message ( diagnostic . message ) ; } }
function showInIssueList ( diagnostic ) { return diagnostic . severity === "error" || diagnostic . rule !== "ordinary-junction-surface" ; }
function setup ( ) { areaLabel . textContent = state . areaId ; diagnostics . innerHTML = "" ; const issues = state . compiled . diagnostics . filter ( showInIssueList ) ; if ( ! issues . length ) diagnostics . innerHTML = "<li>没有需要人工检查的问题。</li>" ; for ( const diagnostic of issues ) { const item = document . createElement ( "li" ) ; const button = document . createElement ( "button" ) ; button . className = diagnostic . severity === "error" ? "error" : "" ; button . textContent = diagnostic . message ; button . onclick = ( ) => focusDiagnostic ( diagnostic ) ; item . append ( button ) ; diagnostics . append ( item ) ; } setBounds ( ) ; resize ( ) ; select ( null ) ; }
fetch ( "/api/state" ) . then ( ( response ) => response . json ( ) ) . then ( ( value ) => { state = value ; setup ( ) ; message ( ` ${ state . compiled . model . roads . length } 条方向道路, ${ state . compiled . diagnostics . length } 个待检查项 ` ) ; } ) . catch ( ( error ) => message ( error . message ) ) ;
window . onresize = resize ;
function effectiveConnectorEnabled ( connector ) { const connection = state ? . compiled . model . connections . find ( ( item ) => item . id === connector . connection _id ) ; return effectiveLaneEnabled ( connector ) && ( ! connection || effectiveConnectionEnabled ( connection ) ) ; }
function selectRoad ( road ) {
selectedRoad = road ; layers . osm . changed ( ) ; layers . lanes . changed ( ) ; layers . connectors . changed ( ) ;
form . hidden = ! road ; hint . h idden = Boolean ( road ) ; if ( ! road ) return ;
roadName . textContent = ` ${ roadLabel ( road ) } ( ${ road . direction === "forward" ? "沿 OSM 节点顺序" : "逆 OSM 节点顺序" } ) ` ;
widthInput . value = road . widthMeters ; lanesInput . value = road . laneCount ; leftInput . checked = road . sidewalkLeft ; rightInput . checked = road . sidewalkRight ;
evidence . textContent = JSON . stringify ( { OSM道路 : road . osmWayIds , 参数来源 : road . provenance , 已应用修改 : road . appliedOverrideIds , 原始标签 : road . tags } , null , 2 ) ;
renderDirectionSwitch ( road ) ; renderMovementSummary ( road ) ; renderConnections ( road ) ; message ( ` 已选中: ${ roadLabel ( road ) } ` ) ;
}
function renderDirectionSwitch ( road ) {
directionSwitch . innerHTML = "" ; const alternatives = state . compiled . model . roads . filter ( ( item ) => item . osmWayIds . join ( "," ) === road . osmWayIds . join ( "," ) ) ;
if ( alternatives . length < 2 ) { directionSwitch . textContent = "单向道路" ; return ; }
for ( const item of alternatives ) { const button = document . createElement ( "button" ) ; button . type = "button" ; button . textContent = item . direction === "forward" ? "沿 OSM 节点顺序" : "逆 OSM 节点顺序" ; button . disabled = item . id === road . id ; button . onclick = ( ) => selectRoad ( item ) ; directionSwitch . append ( button ) ; }
}
function renderMovementSummary ( road ) { const connectors = state . layers . connectors ? . features . filter ( ( feature ) => roadIdFromLane ( feature . properties . from _lane _id ) === road . id && effectiveConnectorEnabled ( feature . properties ) ) || [ ] ; const turns = connectors . reduce ( ( result , item ) => { result [ item . properties . turn ] = ( result [ item . properties . turn ] || 0 ) + 1 ; return result ; } , { } ) ; const labels = { left : "左转" , through : "直行" , right : "右转" , uturn : "掉头" } ; movementSummary . textContent = connectors . length ? ` 已生成 ${ connectors . length } 条路径: ${ Object . entries ( turns ) . map ( ( [ key , value ] ) => ` ${ labels [ key ] || key } ${ value } ` ) . join ( ", " ) } ` : "当前方向没有已生成的转向路径" ; }
function renderConnections ( road ) {
connectionsBox . innerHTML = "" ; const endpoint = state . compiled . model . endpoints . find ( ( item ) => item . roadId === road . id && item . side === "end" ) ; addConnectionButton . hidden = ! endpoint ; const rows = state . compiled . model . connections . filter ( ( connection ) => connection . fromEndpointId === endpoint ? . id ) ;
if ( ! rows . length ) { connectionsBox . textContent = "当前方向到达终点后没有候选驶出道路。" ; return ; }
for ( const connection of rows ) { const target = state . compiled . model . roads . find ( ( item ) => item . id === state . compiled . model . endpoints . find ( ( endpointItem ) => endpointItem . id === connection . toEndpointId ) ? . roadId ) ; if ( ! target ) continue ; const label = document . createElement ( "label" ) ; const input = document . createElement ( "input" ) ; input . type = "checkbox" ; input . checked = effectiveConnectionEnabled ( connection ) ; input . onchange = ( ) => { stageConnection ( connection , input . checked ) ; selectRoad ( road ) ; } ; label . append ( input , ` ${ turnName ( road , target ) } : ${ roadLabel ( target ) } ` ) ; connectionsBox . append ( label ) ; renderLaneControls ( connection ) ; }
}
function renderLaneControls ( connection ) { const rows = state . layers . connectors ? . features . filter ( ( feature ) => feature . properties . connection _id === connection . id ) . map ( ( feature ) => feature . properties ) || [ ] ; for ( const row of rows ) { const label = document . createElement ( "label" ) ; const input = document . createElement ( "input" ) ; input . type = "checkbox" ; input . checked = effectiveLaneEnabled ( row ) ; input . onchange = ( ) => { stageLaneConnection ( row , input . checked ) ; selectRoad ( selectedRoad ) ; } ; label . append ( input , ` 第 ${ row . from _lane _id . split ( ":" ) . at ( - 1 ) } 车道 → 第 ${ row . to _lane _id . split ( ":" ) . at ( - 1 ) } 车道 ` ) ; connectionsBox . append ( label ) ; } }
function turnName ( from , to ) { const heading = ( a , b ) => Math . atan2 ( b [ 0 ] - a [ 0 ] , b [ 1 ] - a [ 1 ] ) * 180 / Math . PI ; const delta = ( ( heading ( to . centerline [ 0 ] , to . centerline [ 1 ] ) - heading ( from . centerline . at ( - 2 ) , from . centerline . at ( - 1 ) ) + 540 ) % 360 ) - 180 ; return Math . abs ( delta ) >= 150 ? "掉头" : Math . abs ( delta ) <= 30 ? "直行" : delta > 0 ? "右转" : "左转" ; }
function stageConnection ( connection , enabled ) { const id = ` 连接: ${ connection . id } ` ; staged = staged . filter ( ( item ) => item . id !== id ) ; staged . push ( { id , kind : "junction-connection" , fromEndpointId : connection . fromEndpointId , toEndpointId : connection . toEndpointId , enabled } ) ; message ( "有未保存修改" ) ; }
function stageLaneConnection ( connector , enabled ) { const id = ` 车道连接: ${ connector . from _lane _id } -> ${ connector . to _lane _id } ` ; staged = staged . filter ( ( item ) => item . id !== id ) ; staged . push ( { id , kind : "lane-connection" , fromLaneId : connector . from _lane _id , toLaneId : connector . to _lane _id , enabled } ) ; message ( "有未保存修改" ) ; }
function renderDiagnostics ( ) { diagnostics . innerHTML = "" ; for ( const item of state . compiled . diagnostics . filter ( ( diagnostic ) => diagnostic . rule !== "ordinary-junction-surface" ) ) { const button = document . createElement ( "button" ) ; button . textContent = item . message ; button . onclick = ( ) => selectRoad ( state . compiled . model . roads . find ( ( road ) => road . id === item . subjectId ) ) ; diagnostics . append ( button ) ; } }
form . onsubmit = ( event ) => { event . preventDefault ( ) ; const id = ` 道路: ${ selectedRoad . id } ` ; staged = staged . filter ( ( item ) => item . id !== id ) ; staged . push ( { id , kind : "road" , roadId : selectedRoad . id , widthMeters : Number ( widthInput . value ) , laneCount : Number ( lanesInput . value ) , sidewalkLeft : leftInput . checked , sidewalkRight : rightInput . checked } ) ; message ( "有未保存修改" ) ; } ;
saveButton . onclick = async ( ) => { const existing = state . overrides . overrides . filter ( ( item ) => ! staged . some ( ( change ) => change . id === item . id ) ) ; const response = await fetch ( "/api/overrides" , { method : "POST" , headers : { "Content-Type" : "application/json" } , body : JSON . stringify ( { schema : "native-road-overrides/v1" , overrides : [ ... existing , ... staged ] } ) } ) ; const result = await response . json ( ) ; if ( ! result . ok ) return message ( result . error ) ; state . overrides = result . overrides ; staged = [ ] ; message ( "已保存,点击“保存并重新生成”写入几何" ) ; } ;
compileButton . onclick = async ( ) => { message ( "正在重新生成..." ) ; const response = await fetch ( "/api/compile" , { method : "POST" } ) ; state = await response . json ( ) ; staged = [ ] ; updateSources ( ) ; renderDiagnostics ( ) ; selectRoad ( selectedRoad ? state . compiled . model . roads . find ( ( road ) => road . id === selectedRoad . id ) : null ) ; message ( "已重新生成" ) ; } ;
for ( const input of document . querySelectorAll ( "[data-layer]" ) ) input . onchange = ( ) => { layers [ input . dataset . layer ] . setVisible ( input . checked ) ; } ;
fetch ( "/api/state" ) . then ( ( response ) => response . json ( ) ) . then ( ( value ) => { state = value ; updateSources ( ) ; renderDiagnostics ( ) ; areaLabel . textContent = state . areaId ; message ( ` 已加载 ${ state . compiled . model . roads . length } 条方向道路 ` ) ; } ) . catch ( ( error ) => message ( error . message ) ) ;