@@ -2,31 +2,120 @@
const fs = require ( "fs" ) ;
const { parseOsm } = require ( "./osm" ) ;
const {
appendCoordinates ,
haversineMeters ,
laneCenterline ,
lateralOffsetFrom ,
orientPolyline ,
polylineLength ,
polylineMidpoint ,
} = require ( "./lane-geometry" ) ;
const MAX _ROUTES = 5 ;
const MAX _PATH _EDGES = 7 ;
const MIN _ROUTE _EDGES = 3 ;
const LANE _OFFSET _METERS = 1.3 ;
const MAX _ LANE_DISTANCE _METERS = 20 ;
const MIN _LATERAL _SEPARATION _METERS = 0.25 ;
const JUNCTION _TRIM _METERS = 6.0 ;
const CONNECTOR _SURFACE _TOLERANCE _METERS = 0.35 ;
const ALL _TURNS = new Set ( [ "left" , "through" , "right" ] ) ;
function buildVehicleRoute ( osmPath ) {
function buildVehicleRoute ( osmPath , lanePolygonsPath , networkPath , intersectionSurfacePath ) {
if ( ! lanePolygonsPath || ! networkPath || ! intersectionSurfacePath ) {
throw new Error ( "Lane polygons, osm2streets network, and intersection surface paths are required for vehicle route generation" ) ;
}
const osm = parseOsm ( fs . readFileSync ( osmPath , "utf8" ) ) ;
const edges = directedRoadEdges ( osm . ways , osm . nodes , osm . bounds ) ;
const routes = selectRoutes ( findReturnRoutes ( edges ) ) ;
const lanePolygons = readLanePolygons ( lanePolygonsPath ) ;
const network = readJsonObject ( networkPath , "osm2streets network" ) ;
const intersectionSurfaces = readFeatureCollection ( intersectionSurfacePath , "intersection surfaces" ) ;
const diagnostics = [ ] ;
const laneIndex = indexDrivingLanes ( lanePolygons . features , diagnostics ) ;
const intersections = indexIntersections ( network , intersectionSurfaces . features ) ;
const edges = directedRoadEdges ( network , osm . ways , diagnostics ) ;
const candidates = findReturnRoutes ( edges ) ;
const routes = [ ] ;
for ( const candidate of candidates ) {
const route = makeRoute ( candidate , laneIndex , intersections , diagnostics ) ;
if ( route ) routes . push ( route ) ;
}
const selected = selectRoutes ( routes ) ;
return {
source : osmPath ,
laneSource : lanePolygonsPath ,
networkSource : networkPath ,
intersectionSource : intersectionSurfacePath ,
bounds : osm . bounds ,
generatedAt : new Date ( ) . toISOString ( ) ,
speedMetersPerSecond : 8.0 ,
loop : true ,
routes ,
// Older previews read `segments`; keep it as an alias while new previews
// use the more accurate route name.
segments : rou tes ,
routes : selected ,
diagnostics ,
// 旧预览仍读取 segments; 保持与 routes 为同一个数组引用。
segments : selec ted ,
} ;
}
function readLanePolygons ( file ) {
return readFeatureCollection ( file , "lane polygons" ) ;
}
function readFeatureCollection ( file , label ) {
const collection = readJsonObject ( file , label ) ;
if ( collection ? . type !== "FeatureCollection" || ! Array . isArray ( collection . features ) ) {
throw new Error ( ` Invalid ${ label } GeoJSON ' ${ file } ': expected FeatureCollection ` ) ;
}
return collection ;
}
function readJsonObject ( file , label ) {
try {
const value = JSON . parse ( fs . readFileSync ( file , "utf8" ) ) ;
if ( ! value || typeof value !== "object" || Array . isArray ( value ) ) throw new Error ( "expected JSON object" ) ;
return value ;
} catch ( error ) {
throw new Error ( ` Invalid ${ label } JSON ' ${ file } ': ${ error . message } ` ) ;
}
}
function indexDrivingLanes ( features , diagnostics ) {
const index = new Map ( ) ;
features . forEach ( ( feature , featureIndex ) => {
if ( feature ? . properties ? . type !== "Driving" ) return ;
const centerline = laneCenterline ( feature ) ;
const direction = feature . properties . direction ;
const widthMeters = Number ( feature . properties . width ) ;
const road = Number ( feature . properties . road ) ;
if ( ! centerline || ! [ "Fwd" , "Back" ] . includes ( direction ) || ! Number . isFinite ( widthMeters ) || widthMeters <= 0 || ! Number . isInteger ( road ) ) {
diagnostics . push ( {
reason : "invalid_lane_polygon" ,
featureIndex ,
road : feature ? . properties ? . road ? ? null ,
laneIndex : feature ? . properties ? . index ? ? null ,
} ) ;
return ;
}
const lane = {
featureIndex ,
polygonId : feature . id ? ? ` ${ feature . properties . road ? ? "road" } : ${ direction } : ${ feature . properties . index ? ? featureIndex } ` ,
road ,
laneIndex : feature . properties . index ,
widthMeters ,
allowedTurns : normalizeAllowedTurns ( feature . properties . allowed _turns ) ,
centerline ,
} ;
const key = laneKey ( road , direction ) ;
if ( ! index . has ( key ) ) index . set ( key , [ ] ) ;
index . get ( key ) . push ( lane ) ;
} ) ;
return index ;
}
function normalizeAllowedTurns ( value ) {
if ( ! Array . isArray ( value ) ) return new Set ( ) ;
return new Set ( value . map ( normalizeTurn ) . filter ( Boolean ) ) ;
}
function isCruiseHighway ( tags ) {
const highway = tags . highway || "" ;
if ( ! highway || tags . area === "yes" ) return false ;
@@ -36,70 +125,100 @@ function isCruiseHighway(tags) {
] ) . has ( highway ) ;
}
function directedRoadEdges ( ways , node s, bound s) {
function directedRoadEdges ( network , way s, diagnostic s) {
if ( ! Array . isArray ( network . roads ) || ! network . gps _bounds ) {
throw new Error ( "Invalid osm2streets network: expected roads and gps_bounds" ) ;
}
const waysById = new Map ( ways . map ( ( way ) => [ String ( way . id ) , way ] ) ) ;
const edges = [ ] ;
for ( const wa y of way s) {
if ( ! isCruiseHighway ( way . tags ) ) continue ;
const refs = compactRefs ( way . refs ) ;
if ( refs . length < 2 ) continue ;
const coord s = ref s. map ( ( ref ) => nodes . get ( ref ) ) ;
if ( ! routeInsideBounds ( coords , bounds ) || r outeLength ( coords ) < 12 ) continue ;
const oneway = String ( way . tags . oneway || "" ) . toLowerCase ( ) ;
if ( oneway !== "-1" ) edges . push ( makeEdge ( way , refs , coords , "forward" ) ) ;
if ( ! isOneWay ( oneway ) ) {
edges . push ( makeEdge ( way , [ ... refs ] . reverse ( ) , [ ... coords ] . reverse ( ) , "backward" ) ) ;
for ( const entr y of network . road s) {
const road = Array . isArray ( entry ) ? entry [ 1 ] : null ;
if ( ! road || ! Number . isInteger ( Number ( road . id ) ) || ! Array . isArray ( road . lane _specs _ltr ) ) continue ;
const wayIds = Array . isArray ( road . osm _ids ) ? road . osm _ids . map ( String ) : [ ] ;
const sourceWay s = wayId s. map ( ( id ) => waysById . get ( id ) ) . filter ( Boolean ) ;
const sourceWay = s ourceWays [ 0 ] || null ;
const tags = sourceWay ? . tags || { highway : road . highway _type || "" } ;
if ( ! isCruiseHighway ( tags ) ) continue ;
if ( sourceWays . length > 1 && sourceWays . some ( ( way ) => JSON . stringify ( way . tags ) !== JSON . stringify ( sourceWay . tags ) ) ) {
addDiagnostic ( diagnostics , { reason : "ambiguous_internal_road_source" , road : road . id , osmWayIds : wayIds } ) ;
continue ;
}
const coordinates = networkPolylineToGps ( road . center _line , network . gps _bounds ) ;
if ( coordinates . length < 2 || routeLength ( coordinates ) < 12 ) continue ;
const directions = new Set ( road . lane _specs _ltr
. filter ( ( lane ) => lane . lt === "Driving" )
. map ( ( lane ) => lane . dir ) ) ;
if ( directions . has ( "Fwd" ) ) edges . push ( makeEdge ( road , sourceWay , wayIds , coordinates , "forward" ) ) ;
if ( directions . has ( "Back" ) ) edges . push ( makeEdge ( road , sourceWay , wayIds , [ ... coordinates ] . reverse ( ) , "backward" ) ) ;
}
return edges . sort ( ( a , b ) => a . id . localeCompare ( b . id ) ) ;
}
function makeEdge ( way , ref s, coordinates , direction ) {
function makeEdge ( road , way, wayId s, coordinates , direction ) {
const forward = direction === "forward" ;
const tags = way ? . tags || { } ;
return {
id : ` ${ way . id } : ${ direction } ` ,
way Id: way . id ,
id : ` road- ${ road . id } : ${ direction } ` ,
road Id: Number ( road . id ) ,
wayId : wayIds [ 0 ] || "" ,
osmWayIds : wayIds ,
direction ,
name : way . tags . name || way . tags . highway || "road" ,
highway : way . tags . highway || "" ,
oneWay : way . tags . oneway || "" ,
startNode : refs [ 0 ] ,
endNode : refs [ refs . length - 1 ] ,
name : road . name || tags . name || road . highway _type || "road" ,
highway : road . highway _type || tags . highway || "" ,
oneWay : directionsForRoad ( road ) . size === 1 ? "yes" : "" ,
startNode : forward ? Number ( road . src _i ) : Number ( road . dst _i ) ,
endNode : forward ? Number ( road . dst _i ) : Number ( road . src _i ) ,
coordinates ,
allowedTurns : allowedTurns ( way . tags, direction ) ,
allowedTurns : allowedTurns ( tags , direction ) ,
turnLanes : turnLanes ( tags , direction ) ,
} ;
}
function directionsForRoad ( road ) {
return new Set ( road . lane _specs _ltr . filter ( ( lane ) => lane . lt === "Driving" ) . map ( ( lane ) => lane . dir ) ) ;
}
function networkPolylineToGps ( polyline , bounds ) {
const points = Array . isArray ( polyline ? . pts ) ? polyline . pts : [ ] ;
const widthMeters = haversineMeters ( [ bounds . min _lon , bounds . min _lat ] , [ bounds . max _lon , bounds . min _lat ] ) ;
const heightMeters = haversineMeters ( [ bounds . min _lon , bounds . min _lat ] , [ bounds . min _lon , bounds . max _lat ] ) ;
if ( ! ( widthMeters > 0 ) || ! ( heightMeters > 0 ) ) return [ ] ;
return points . map ( ( point ) => {
const x = Number ( point . x ) / 10000 ;
const y = Number ( point . y ) / 10000 ;
return [
bounds . min _lon + x / widthMeters * ( bounds . max _lon - bounds . min _lon ) ,
bounds . min _lat + ( bounds . max _lat - bounds . min _lat ) * ( heightMeters - y ) / heightMeters ,
] ;
} ) . filter ( ( coordinate ) => coordinate . every ( Number . isFinite ) ) ;
}
function isOneWay ( value ) {
return [ "yes" , "true" , "1" ] . includes ( value ) ;
}
function compactRefs ( refs ) {
return refs . filter ( ( ref , index ) => index === 0 || ref !== refs [ index - 1 ] ) ;
}
function routeInsideBounds ( coords , bounds ) {
if ( ! bounds ) return true ;
return coords . some ( ( coord ) => insideBounds ( coord , bounds ) ) ;
}
function insideBounds ( coord , bounds ) {
const pad = 0.00002 ;
return coord [ 0 ] >= bounds . minLon - pad && coord [ 0 ] <= bounds . maxLon + pad &&
coord [ 1 ] >= bounds . minLat - pad && coord [ 1 ] <= bounds . maxLat + pad ;
}
function allowedTurns ( tags , direction ) {
const value = tags [ ` turn:lanes: ${ direction } ` ] || tags [ "turn:lanes" ] ;
if ( ! value ) return ALL _TURNS ;
const turns = new Set ( ) ;
for ( const lane of String ( value ) . split ( "|" ) ) {
for ( const maneuver of lane . split ( ";" ) ) {
const normalized = maneuver . trim ( ) . replace ( /^slight_/ , "" ) ;
if ( ALL _TURNS . has ( normalized ) ) turns . add ( normalized ) ;
}
}
const lanes = turnLanes ( tags , direction ) ;
if ( ! lanes ) return ALL _TURNS ;
const turns = new Set ( lanes . flatMap ( ( lane ) => [ ... lane ] ) . filter ( ( turn ) => ALL _TURNS . has ( turn ) ) ) ;
return turns . size ? turns : ALL _TURNS ;
}
function turnLanes ( tags , direction ) {
const value = tags [ ` turn:lanes: ${ direction } ` ] ? ? tags [ "turn:lanes" ] ;
if ( value === undefined || value === "" ) return null ;
return String ( value ) . split ( "|" ) . map ( ( lane ) => {
const turns = new Set ( String ( lane ) . split ( ";" ) . map ( normalizeTurn ) . filter ( Boolean ) ) ;
return turns . size ? turns : new Set ( ALL _TURNS ) ;
} ) ;
}
function normalizeTurn ( value ) {
const turn = String ( value || "" ) . trim ( ) . replace ( /^slight_/ , "" ) ;
if ( turn === "reverse" ) return "u_turn" ;
return [ ... ALL _TURNS , "u_turn" ] . includes ( turn ) ? turn : null ;
}
function findReturnRoutes ( edges ) {
const outgoing = new Map ( ) ;
const byId = new Map ( ) ;
@@ -110,14 +229,12 @@ function findReturnRoutes(edges) {
}
const candidates = [ ] ;
const seen = new Set ( ) ;
for ( const first of edges ) {
walkToTerminal ( [ first ] , [ ] , outgoing , byId , candidates , seen ) ;
}
for ( const first of edges ) walkToTerminal ( [ first ] , [ ] , outgoing , byId , candidates , seen ) ;
return candidates . sort ( ( a , b ) => a . signature . localeCompare ( b . signature ) ) ;
}
function walkToTerminal ( path , maneuvers , outgoing , byId , candidates , seen ) {
const current = path [ p ath . length - 1 ] ;
const current = path . at ( - 1 ) ;
if ( path . length >= MIN _ROUTE _EDGES ) {
const route = returnRoute ( path , maneuvers , byId ) ;
if ( route && ! seen . has ( route . signature ) ) {
@@ -139,7 +256,7 @@ function walkToTerminal(path, maneuvers, outgoing, byId, candidates, seen) {
}
function classifyConnection ( incoming , outgoing ) {
if ( incoming . way Id === outgoing . way Id) return null ;
if ( incoming . road Id === outgoing . road Id) return null ;
const inVector = directionVector ( incoming . coordinates . at ( - 2 ) , incoming . coordinates . at ( - 1 ) ) ;
const outVector = directionVector ( outgoing . coordinates [ 0 ] , outgoing . coordinates [ 1 ] ) ;
const dot = inVector . x * outVector . x + inVector . y * outVector . y ;
@@ -151,7 +268,7 @@ function classifyConnection(incoming, outgoing) {
}
function directionVector ( a , b ) {
const scale = 111320.0 ;
const scale = 111320;
const x = ( b [ 0 ] - a [ 0 ] ) * scale * Math . cos ( degreesToRadians ( ( a [ 1 ] + b [ 1 ] ) / 2 ) ) ;
const y = ( b [ 1 ] - a [ 1 ] ) * scale ;
const length = Math . hypot ( x , y ) || 1 ;
@@ -159,7 +276,7 @@ function directionVector(a, b) {
}
function returnRoute ( path , forwardManeuvers , byId ) {
const reverse = path . slice ( ) . reverse ( ) . map ( ( edge ) => byId . get ( ` ${ edge . way Id} : ${ oppositeDirection ( edge . direction ) } ` ) ) ;
const reverse = path . slice ( ) . reverse ( ) . map ( ( edge ) => byId . get ( ` road- ${ edge . road Id} : ${ oppositeDirection ( edge . direction ) } ` ) ) ;
if ( reverse . some ( ( edge ) => ! edge ) ) return null ;
const returnManeuvers = [ ] ;
for ( let index = 1 ; index < reverse . length ; index += 1 ) {
@@ -167,67 +284,312 @@ function returnRoute(path, forwardManeuvers, byId) {
if ( ! maneuver ) return null ;
returnManeuvers . push ( maneuver ) ;
}
const signature = path . map ( ( edge ) => edge . wayId ) . sort ( ) . join ( ">" ) ;
return makeRoute (
[ ... path , ... reverse ] ,
[ ... forwardManeuvers , "u_turn" , ... returnManeuvers , "u_turn" ] ,
const signature = path . map ( ( edge ) => edge . roadId ) . join ( ">" ) ;
return {
edges : [ ... path , ... reverse ] ,
maneuvers : [ ... forwardManeuvers , "u_turn" , ... returnManeuvers , "u_turn" ] ,
forwardEdgeCount : path . length ,
signature ,
) ;
} ;
}
function oppositeDirection ( direction ) {
return direction === "forward" ? "backward" : "forward" ;
}
function makeRoute ( edges , m aneuvers , signature ) {
const coordinates = smoothRoute ( edges ) ;
function makeRoute ( candidate , l aneIndex , intersections , diagnostics ) {
const selectedLanes = [ ] ;
for ( let index = 0 ; index < candidate . edges . length ; index += 1 ) {
const edge = candidate . edges [ index ] ;
const match = selectLaneForEdge ( edge , candidate . maneuvers [ index ] , laneIndex , {
// 仅去程中的真实路口受 turn:lanes 严格约束;端点调头与展示返程不能被反向标签否决。
enforceTurnRestrictions : index < candidate . forwardEdgeCount - 1 ,
} ) ;
if ( ! match . ok ) {
addDiagnostic ( diagnostics , {
reason : match . reason ,
routeSignature : candidate . signature ,
edgeId : edge . id ,
road : edge . roadId ,
osmWayId : edge . wayId ,
direction : edge . direction ,
maneuver : candidate . maneuvers [ index ] ,
detail : match . detail ,
} ) ;
return null ;
}
selectedLanes . push ( match . lane ) ;
}
const smoothed = smoothLaneRoute ( candidate . edges , selectedLanes , intersections ) ;
if ( ! smoothed . ok ) {
addDiagnostic ( diagnostics , { reason : smoothed . reason , routeSignature : candidate . signature , ... smoothed . detail } ) ;
return null ;
}
const coordinates = smoothed . coordinates ;
const centerlineCoordinates = smoothRoute ( candidate . edges ) ;
const route = {
id : ` route- ${ signature . replace ( /[^\w]+/g , "-" ) } ` ,
highway : edges [ 0 ] . highway ,
oneWay : edges . some ( ( edge ) => isOneWay ( String ( edge . oneWay ) . toLowerCase ( ) ) ) ? "partial" : "" ,
edgeIds : edges . map ( ( edge ) => edge . id ) ,
maneuvers ,
id : ` route- ${ candidate . signature. replace ( /[^\w]+/g , "-" ) } ` ,
highway : candidate . edges[ 0 ] . highway ,
oneWay : candidate . edges. some ( ( edge ) => isOneWay ( String ( edge . oneWay ) . toLowerCase ( ) ) ) ? "partial" : "" ,
edgeIds : candidate . edges. map ( ( edge ) => edge . id ) ,
maneuvers : candidate . maneuvers ,
lengthMeters : routeLength ( coordinates ) ,
laneOffsetMeters : LANE _OFFSET _METERS ,
coordinates: offsetClosedRouteRight ( coordinates , LANE _OFFSET _METERS ) ,
centerlineCoordinates : coordinates ,
coordinates ,
centerlineC oordinates ,
laneSegments : selectedLanes . flatMap ( ( lane , edgeIndex ) => lane . fragments . map ( ( fragment ) => ( {
edgeId : candidate . edges [ edgeIndex ] . id ,
osmWayId : candidate . edges [ edgeIndex ] . wayId ,
direction : candidate . edges [ edgeIndex ] . direction ,
laneIndex : lane . laneIndex ,
widthMeters : fragment . widthMeters ,
centerOffsetMeters : Number ( fragment . centerOffsetMeters . toFixed ( 3 ) ) ,
maneuver : candidate . maneuvers [ edgeIndex ] ,
source : "lane_polygon_centerline" ,
polygonId : fragment . polygonId ,
featureIndex : fragment . featureIndex ,
road : fragment . road ,
} ) ) ) ,
connectors : smoothed . connectors ,
} ;
Object . defineProperty ( route , "signature" , { value : signature } ) ;
Object . defineProperty ( route , "signature" , { value : candidate . signature } ) ;
return route ;
}
function selectLaneForEdge ( edge , maneuver , laneIndex , options = { } ) {
const enforceTurnRestrictions = options . enforceTurnRestrictions !== false ;
const expectedDirection = edge . direction === "forward" ? "Fwd" : "Back" ;
const candidates = laneIndex . get ( laneKey ( edge . roadId , expectedDirection ) ) || [ ] ;
if ( ! candidates . length ) return { ok : false , reason : "missing_lane_polygon" } ;
const lanes = [ ] ;
for ( const fragment of candidates ) {
const centerline = orientPolyline ( fragment . centerline , edge . coordinates ) ;
if ( ! centerline ) return { ok : false , reason : "invalid_lane_polygon" , detail : "direction_alignment" } ;
const midpoint = polylineMidpoint ( centerline ) ;
const offset = lateralOffsetFrom ( edge . coordinates , midpoint ) ;
if ( ! offset || offset . distance > MAX _LANE _DISTANCE _METERS ) {
return { ok : false , reason : "missing_lane_polygon" , detail : "geometry_too_far_from_internal_road" } ;
}
lanes . push ( {
laneIndex : fragment . laneIndex ,
centerline ,
centerOffsetMeters : offset . lateral ,
allowedTurns : fragment . allowedTurns ,
fragments : [ { ... fragment , centerline , centerOffsetMeters : offset . lateral } ] ,
} ) ;
}
lanes . sort ( ( a , b ) => a . centerOffsetMeters - b . centerOffsetMeters || String ( a . laneIndex ) . localeCompare ( String ( b . laneIndex ) ) ) ;
for ( let index = 1 ; index < lanes . length ; index += 1 ) {
if ( lanes [ index ] . centerOffsetMeters - lanes [ index - 1 ] . centerOffsetMeters < MIN _LATERAL _SEPARATION _METERS ) {
return { ok : false , reason : "ambiguous_lane_order" } ;
}
}
if ( enforceTurnRestrictions && edge . turnLanes && edge . turnLanes . length !== lanes . length ) {
return { ok : false , reason : "ambiguous_lane_order" , detail : "turn_lane_count_mismatch" } ;
}
let compatible = lanes . filter ( ( lane , index ) => laneSupportsManeuver ( lane , edge . turnLanes ? . [ index ] , maneuver ) ) ;
if ( ! compatible . length && ! enforceTurnRestrictions ) compatible = lanes ;
if ( ! compatible . length ) return { ok : false , reason : "no_compatible_turn_lane" } ;
const chooseLeft = maneuver === "left" || maneuver === "u_turn" ;
return { ok : true , lane : chooseLeft ? compatible [ 0 ] : compatible . at ( - 1 ) } ;
}
function laneSupportsManeuver ( lane , osmTurns , maneuver ) {
const expected = maneuver === "u_turn" ? "left" : maneuver ;
if ( osmTurns && ! osmTurns . has ( expected ) && ! ( maneuver === "u_turn" && osmTurns . has ( "u_turn" ) ) ) return false ;
if ( lane . allowedTurns . size && ! lane . allowedTurns . has ( expected ) && ! ( maneuver === "u_turn" && lane . allowedTurns . has ( "u_turn" ) ) ) return false ;
return true ;
}
function laneKey ( roadId , direction ) {
return ` ${ String ( roadId ) } : ${ direction } ` ;
}
function addDiagnostic ( diagnostics , entry ) {
const key = JSON . stringify ( entry ) ;
if ( ! diagnostics . some ( ( current ) => JSON . stringify ( current ) === key ) ) diagnostics . push ( entry ) ;
}
function indexIntersections ( network , features ) {
if ( ! Array . isArray ( network . intersections ) ) throw new Error ( "Invalid osm2streets network: expected intersections" ) ;
const surfaces = new Map ( features
. filter ( ( feature ) => feature ? . geometry ? . type === "Polygon" && Number . isInteger ( Number ( feature . properties ? . id ) ) )
. map ( ( feature ) => [ Number ( feature . properties . id ) , feature . geometry . coordinates [ 0 ] ] ) ) ;
const intersections = new Map ( ) ;
for ( const entry of network . intersections ) {
const intersection = Array . isArray ( entry ) ? entry [ 1 ] : null ;
if ( ! intersection || ! Number . isInteger ( Number ( intersection . id ) ) ) continue ;
intersections . set ( Number ( intersection . id ) , {
id : Number ( intersection . id ) ,
osmNodeIds : Array . isArray ( intersection . osm _ids ) ? intersection . osm _ids . map ( String ) : [ ] ,
surface : surfaces . get ( Number ( intersection . id ) ) || null ,
} ) ;
}
return intersections ;
}
function smoothLaneRoute ( edges , selectedLanes , intersections ) {
const route = [ ] ;
const connectors = [ ] ;
for ( let index = 0 ; index < edges . length ; index += 1 ) {
const current = selectedLanes [ index ] . centerline ;
appendCoordinates ( route , current ) ;
const nextIndex = ( index + 1 ) % edges . length ;
const next = selectedLanes [ nextIndex ] . centerline ;
const incomingEdge = edges [ index ] ;
const outgoingEdge = edges [ nextIndex ] ;
if ( incomingEdge . endNode !== outgoingEdge . startNode ) {
return { ok : false , reason : "disconnected_internal_roads" , detail : { fromRoad : incomingEdge . roadId , toRoad : outgoingEdge . roadId } } ;
}
const intersection = intersections . get ( incomingEdge . endNode ) ;
if ( ! intersection ? . surface ) {
return { ok : false , reason : "missing_intersection_surface" , detail : { intersectionId : incomingEdge . endNode } } ;
}
const isUTurn = incomingEdge . roadId === outgoingEdge . roadId ;
const turn = constrainedConnector ( current , next , incomingEdge . coordinates . at ( - 1 ) , intersection . surface , isUTurn ) ;
if ( ! turn ) {
return {
ok : false ,
reason : "connector_outside_intersection" ,
detail : { intersectionId : intersection . id , fromRoad : incomingEdge . roadId , toRoad : outgoingEdge . roadId } ,
} ;
}
appendCoordinates ( route , turn . slice ( 1 ) ) ;
connectors . push ( {
intersectionId : intersection . id ,
osmNodeIds : intersection . osmNodeIds ,
fromRoad : incomingEdge . roadId ,
toRoad : outgoingEdge . roadId ,
maneuver : isUTurn ? "u_turn" : classifyConnection ( incomingEdge , outgoingEdge ) ,
source : "intersection_surface_constrained" ,
coordinates : turn ,
} ) ;
}
if ( route . length ) route [ route . length - 1 ] = [ ... route [ 0 ] ] ;
return { ok : true , coordinates : route , connectors } ;
}
function constrainedConnector ( incoming , outgoing , junction , surface , isUTurn ) {
const scales = isUTurn ? [ 1 , 0.8 , 0.6 , 0.4 , 0.25 ] : [ 1 , 0.75 , 0.5 , 0.3 , 0.15 ] ;
for ( const scale of scales ) {
const connector = isUTurn
? uTurnConnector ( incoming , outgoing , junction , 20 , scale )
: tangentBezierTurn ( incoming , outgoing , 16 , scale ) ;
if ( connector . length && connector . every ( ( point ) => pointInPolygonOrNear ( point , surface , CONNECTOR _SURFACE _TOLERANCE _METERS ) ) ) {
return connector ;
}
}
return null ;
}
function pointInPolygonOrNear ( point , ring , toleranceMeters ) {
if ( ! Array . isArray ( ring ) || ring . length < 4 ) return false ;
let inside = false ;
for ( let i = 0 , j = ring . length - 1 ; i < ring . length ; j = i , i += 1 ) {
const a = ring [ i ] ;
const b = ring [ j ] ;
if ( ( a [ 1 ] > point [ 1 ] ) !== ( b [ 1 ] > point [ 1 ] ) &&
point [ 0 ] < ( b [ 0 ] - a [ 0 ] ) * ( point [ 1 ] - a [ 1 ] ) / ( b [ 1 ] - a [ 1 ] ) + a [ 0 ] ) inside = ! inside ;
if ( distanceToSegmentMeters ( point , a , b ) <= toleranceMeters ) return true ;
}
return inside ;
}
function distanceToSegmentMeters ( point , start , end ) {
const latitude = ( point [ 1 ] + start [ 1 ] + end [ 1 ] ) / 3 ;
const metersLon = 111320 * Math . cos ( degreesToRadians ( latitude ) ) ;
const dx = ( end [ 0 ] - start [ 0 ] ) * metersLon ;
const dy = ( end [ 1 ] - start [ 1 ] ) * 111320 ;
const px = ( point [ 0 ] - start [ 0 ] ) * metersLon ;
const py = ( point [ 1 ] - start [ 1 ] ) * 111320 ;
const lengthSquared = dx * dx + dy * dy ;
const ratio = lengthSquared ? Math . max ( 0 , Math . min ( 1 , ( px * dx + py * dy ) / lengthSquared ) ) : 0 ;
return Math . hypot ( px - dx * ratio , py - dy * ratio ) ;
}
function smoothRoute ( edges ) {
const trimmed = edges . map ( ( edge ) => trimPolyline ( edge . coordinates , JUNCTION _TRIM _METERS ) ) ;
const route = [ ] ;
for ( let index = 0 ; index < edges . length ; index += 1 ) {
appendCoordinates ( route , trimmed [ index ] ) ;
const nextIndex = ( index + 1 ) % edges . length ;
const junctio n = edges [ index ] . coordinat es. at ( - 1 ) ;
const t urn = edges [ index ] . wayId === edges [ nextI ndex] . wayId
? u Turn( trimmed [ index ] . at ( - 1 ) , junction , trimmed [ nextIndex ] [ 0 ] )
: bezierTurn ( trimmed [ index ] . at ( - 1 ) , junction , trimmed [ nextIndex ] [ 0 ] , 6 ) ;
const tur n = edges [ index ] . roadId === edg es[ nextIndex ] . roadId
? uT urnConnector ( trimmed [ index ] , trimmed [ nextIndex ] , edges [ i ndex] . coordinates . at ( - 1 ) )
: tangentBezier Turn( trimmed [ index ] , trimmed [ nextIndex ] ) ;
appendCoordinates ( route , turn . slice ( 1 ) ) ;
}
if ( route . length ) route [ route . length - 1 ] = [ ... route [ 0 ] ] ;
return route ;
}
function uTurn ( start , junction , end ) {
const tangent = directionVector ( start , junction ) ;
const lef t = offsetCoordinate ( junction , - tangent . y * 3.0 , tangent . x * 3.0 ) ;
const right = offsetCoordinate ( junction , tangent . y * 3.0 , - tangent . x * 3. 0 ) ;
return [
start ,
lerpCoordinate ( start , junction , 0.72 ) ,
left ,
right ,
lerpCoordinate ( end , junction , 0.72 ) ,
end ,
] ;
function tangentBezierTurn ( incoming , outgoing , samples = 16 , scale = 1 ) {
if ( incoming . length < 2 || outgoing . length < 2 ) return [ ] ;
const star t = incoming . at ( - 1 ) ;
const end = outgoing [ 0 ] ;
const incomingTangent = directionVector ( incoming . at ( - 2 ) , start ) ;
const outgoingTangent = directionVector ( end , outgoing [ 1 ] ) ;
const incomingSpan = haversineMeters ( incoming . at ( - 2 ) , start ) ;
const outgoingSpan = haversineMeters ( end , outgoing [ 1 ] ) ;
const intersection = intersectTangentRays ( start , end , incomingTangent , outgoingTangent ) ;
let controlA ;
let controlB ;
if ( intersection && intersection . a >= 0 && intersection . b >= 0 ) {
// 两条车道切线的前向交点定义了转弯的几何目标, Bezier 控制点取三分之一距离。
const maxA = Math . min ( 8 , Math . max ( 0.75 , incomingSpan * 2.4 ) ) ;
const maxB = Math . min ( 8 , Math . max ( 0.75 , outgoingSpan * 2.4 ) ) ;
const distanceA = Math . min ( intersection . a , maxA ) * scale ;
const distanceB = Math . min ( intersection . b , maxB ) * scale ;
controlA = offsetCoordinate ( start , incomingTangent . x * distanceA / 3 , incomingTangent . y * distanceA / 3 ) ;
controlB = offsetCoordinate ( end , - outgoingTangent . x * distanceB / 3 , - outgoingTangent . y * distanceB / 3 ) ;
} else {
// 平行、反向或交点在车道后方时,使用受限 fallback, 避免生成反向回环。
const chordMeters = haversineMeters ( start , end ) ;
const controlMeters = boundedControlDistance ( chordMeters , incomingSpan , outgoingSpan , 0.42 , 8 ) * scale ;
controlA = offsetCoordinate ( start , incomingTangent . x * controlMeters , incomingTangent . y * controlMeters ) ;
controlB = offsetCoordinate ( end , - outgoingTangent . x * controlMeters , - outgoingTangent . y * controlMeters ) ;
}
return cubicBezier ( start , controlA , controlB , end , samples ) ;
}
function intersectTangentRays ( start , end , incomingTangent , outgoingTangent ) {
const latitude = ( start [ 1 ] + end [ 1 ] ) / 2 ;
const metersLon = 111320 * Math . cos ( degreesToRadians ( latitude ) ) ;
const qx = ( end [ 0 ] - start [ 0 ] ) * metersLon ;
const qy = ( end [ 1 ] - start [ 1 ] ) * 111320 ;
const cross = incomingTangent . x * outgoingTangent . y - incomingTangent . y * outgoingTangent . x ;
if ( Math . abs ( cross ) < 1e-6 ) return null ;
const crossQOutgoing = qx * outgoingTangent . y - qy * outgoingTangent . x ;
const crossQIncoming = qx * incomingTangent . y - qy * incomingTangent . x ;
return {
a : crossQOutgoing / cross ,
b : crossQIncoming / cross ,
} ;
}
function uTurnConnector ( incoming , outgoing , junction , samples = 20 , scale = 1 ) {
if ( incoming . length < 2 || outgoing . length < 2 ) return [ ] ;
const start = incoming . at ( - 1 ) ;
const end = outgoing [ 0 ] ;
const incomingTangent = directionVector ( incoming . at ( - 2 ) , start ) ;
const outgoingTangent = directionVector ( end , outgoing [ 1 ] ) ;
const chordMeters = haversineMeters ( start , end ) ;
const approachMeters = Math . max ( haversineMeters ( start , junction ) , haversineMeters ( end , junction ) ) ;
const incomingSpan = haversineMeters ( incoming . at ( - 2 ) , start ) ;
const outgoingSpan = haversineMeters ( end , outgoing [ 1 ] ) ;
const availableMeters = Math . max ( 0.5 , Math . min ( 10 , incomingSpan * 0.8 , outgoingSpan * 0.8 ) ) ;
const controlMeters = Math . min ( availableMeters , Math . max ( Math . min ( 2 , availableMeters ) , chordMeters * 1.1 , approachMeters * 0.6 ) ) * scale ;
const controlA = offsetCoordinate ( start , incomingTangent . x * controlMeters , incomingTangent . y * controlMeters ) ;
const controlB = offsetCoordinate ( end , - outgoingTangent . x * controlMeters , - outgoingTangent . y * controlMeters ) ;
return cubicBezier ( start , controlA , controlB , end , samples ) ;
}
function boundedControlDistance ( chordMeters , incomingSpan , outgoingSpan , ratio , maximumMeters ) {
const lowerMeters = Math . min ( 1.5 , chordMeters * 0.35 ) ;
const upperMeters = Math . max ( 0.25 , Math . min ( maximumMeters , chordMeters * 0.65 , incomingSpan * 0.8 , outgoingSpan * 0.8 ) ) ;
return Math . min ( upperMeters , Math . max ( lowerMeters , chordMeters * ratio ) ) ;
}
function offsetCoordinate ( coord , eastMeters , northMeters ) {
const metersPerLat = 111320.0 ;
const metersPerLat = 111320;
const metersPerLon = metersPerLat * Math . cos ( degreesToRadians ( coord [ 1 ] ) ) ;
return [ coord [ 0 ] + eastMeters / metersPerLon , coord [ 1 ] + northMeters / metersPerLat ] ;
}
@@ -249,9 +611,7 @@ function pointAlong(coords, distance) {
return [ ... coords . at ( - 1 ) ] ;
}
function b ezierTurn ( start , junction , end , samples ) {
const controlA = lerpCoordinate ( start , junction , 0.72 ) ;
const controlB = lerpCoordinate ( end , junction , 0.72 ) ;
function cubicB ezier( start , controlA , controlB , end , samples ) {
const points = [ ] ;
for ( let index = 0 ; index <= samples ; index += 1 ) {
const t = index / samples ;
@@ -264,19 +624,6 @@ function bezierTurn(start, junction, end, samples) {
return points ;
}
function appendCoordinates ( target , coordinates ) {
for ( const coord of coordinates ) {
const last = target . at ( - 1 ) ;
if ( ! last || last [ 0 ] !== coord [ 0 ] || last [ 1 ] !== coord [ 1 ] ) target . push ( [ ... coord ] ) ;
}
}
function offsetClosedRouteRight ( coords , offset ) {
const shifted = offsetPolylineRight ( coords , offset ) ;
if ( shifted . length ) shifted [ shifted . length - 1 ] = [ ... shifted [ 0 ] ] ;
return shifted ;
}
function selectRoutes ( candidates ) {
const selected = [ ] ;
const covered = new Set ( ) ;
@@ -295,43 +642,25 @@ function routeScore(route, covered) {
return novelty * 100000 + route . lengthMeters ;
}
function offsetPolylineRight ( coords , offsetMeters ) {
if ( coords . length < 2 || offsetMeters === 0 ) return coords . map ( ( coord ) => [ ... coord ] ) ;
const refLat = coords . reduce ( ( sum , coord ) => sum + coord [ 1 ] , 0 ) / coords . length ;
const metersPerLat = 111320.0 ;
const metersPerLon = 111320.0 * Math . cos ( degreesToRadians ( refLat ) ) ;
const points = coords . map ( ( coord ) => ( { x : coord [ 0 ] * metersPerLon , y : coord [ 1 ] * metersPerLat , lon : coord [ 0 ] , lat : coord [ 1 ] } ) ) ;
return points . map ( ( point , index ) => {
const prev = points [ Math . max ( 0 , index - 1 ) ] ;
const next = points [ Math . min ( points . length - 1 , index + 1 ) ] ;
const length = Math . hypot ( next . x - prev . x , next . y - prev . y ) ;
if ( length < 0.001 ) return [ point . lon , point . lat ] ;
const dx = ( next . x - prev . x ) / length ;
const dy = ( next . y - prev . y ) / length ;
return [ ( point . x + dy * offsetMeters ) / metersPerLon , ( point . y - dx * offsetMeters ) / metersPerLat ] ;
} ) ;
}
function routeLength ( coords ) {
let total = 0 ;
for ( let index = 1 ; index < coords . length ; index += 1 ) total += haversineMeters ( coords [ index - 1 ] , coords [ index ] ) ;
return total ;
}
function haversineMeters ( a , b ) {
const radius = 6371008.8 ;
const lat1 = degreesToRadians ( a [ 1 ] ) ;
const lat2 = degreesToRadians ( b [ 1 ] ) ;
const dLat = degreesToRadians ( b [ 1 ] - a [ 1 ] ) ;
const dLon = degreesToRadians ( b [ 0 ] - a [ 0 ] ) ;
const h = Math . sin ( dLat / 2 ) * * 2 + Math . cos ( lat1 ) * Math . cos ( lat2 ) * Math . sin ( dLon / 2 ) * * 2 ;
return 2 * radius * Math . asin ( Math . min ( 1 , Math . sqrt ( h ) ) ) ;
return polylineLength ( coords ) ;
}
function lerpCoordinate ( a , b , t ) {
return [ a [ 0 ] + ( b [ 0 ] - a [ 0 ] ) * t , a [ 1 ] + ( b [ 1 ] - a [ 1 ] ) * t ] ;
}
function degreesToRadians ( value ) { return value * Math . PI / 180 ; }
function degreesToRadians ( value ) {
return value * Math . PI / 180 ;
}
module . exports = { buildVehicleRoute , classifyConnection , allowedTurns } ;
module . exports = {
allowedTurns ,
buildVehicleRoute ,
classifyConnection ,
readLanePolygons ,
selectLaneForEdge ,
tangentBezierTurn ,
turnLanes ,
uTurnConnector ,
} ;