@drawtonomy/sdk 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/exporter/index.d.ts +4 -1
- package/dist/exporter/index.d.ts.map +1 -1
- package/dist/exporter/index.js +3 -0
- package/dist/exporter/index.js.map +1 -1
- package/dist/exporter/lanelet2.d.ts.map +1 -1
- package/dist/exporter/lanelet2.js +434 -2
- package/dist/exporter/lanelet2.js.map +1 -1
- package/dist/exporter/odrGeometry.d.ts +64 -0
- package/dist/exporter/odrGeometry.d.ts.map +1 -0
- package/dist/exporter/odrGeometry.js +324 -0
- package/dist/exporter/odrGeometry.js.map +1 -0
- package/dist/exporter/odrToShapes.d.ts +45 -0
- package/dist/exporter/odrToShapes.d.ts.map +1 -0
- package/dist/exporter/odrToShapes.js +1370 -0
- package/dist/exporter/odrToShapes.js.map +1 -0
- package/dist/exporter/opendrive.d.ts.map +1 -1
- package/dist/exporter/opendrive.js +493 -68
- package/dist/exporter/opendrive.js.map +1 -1
- package/dist/exporter/opendriveParser.d.ts +186 -0
- package/dist/exporter/opendriveParser.d.ts.map +1 -0
- package/dist/exporter/opendriveParser.js +426 -0
- package/dist/exporter/opendriveParser.js.map +1 -0
- package/dist/exporter/osmToShapes.d.ts +52 -3
- package/dist/exporter/osmToShapes.d.ts.map +1 -1
- package/dist/exporter/osmToShapes.js +208 -5
- package/dist/exporter/osmToShapes.js.map +1 -1
- package/dist/exporter/projection.d.ts +1 -1
- package/dist/exporter/projection.d.ts.map +1 -1
- package/dist/exporter/projection.js +2 -3
- package/dist/exporter/projection.js.map +1 -1
- package/dist/types.d.ts +27 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
// OpenDRIVE 1.8 (.xodr) exporter — emits a road network from a snapshot.
|
|
2
2
|
// No external library dependencies.
|
|
3
3
|
//
|
|
4
|
-
// Design
|
|
5
|
-
// - Each lane is emitted as an independent <road>
|
|
4
|
+
// Design:
|
|
5
|
+
// - Each lane is emitted as an independent <road>
|
|
6
6
|
// - Reference line is the midpoints of the left/right boundary samples
|
|
7
7
|
// - Each segment is a <line> geometry; lane width is a per-sample <width>
|
|
8
|
-
// - Lane connectivity (next/prev) is written into <link
|
|
8
|
+
// - Lane connectivity (next/prev) is written into <link>; branch/merge edges
|
|
9
|
+
// that a single road link cannot express are synthesized into <junction>
|
|
10
|
+
// elements (see planJunctions)
|
|
11
|
+
// - Lanelet-only lane tags are stashed in <userData code="laneAttributes">
|
|
12
|
+
// and restored on import; multi-road signal validity uses <signalReference>
|
|
9
13
|
// - Coordinate frame: canvas (x right, y down) → ENU (x right, y up); y is flipped
|
|
10
14
|
import { computeCenterlineWithWidth } from './laneCenterline';
|
|
11
15
|
import { originToProjString } from './projection';
|
|
@@ -92,7 +96,7 @@ function buildBoundaryAlignmentOverrides(shapeMap, lanes, epsilonPx = 30) {
|
|
|
92
96
|
const pt = shapeMap.get(pid);
|
|
93
97
|
if (!pt)
|
|
94
98
|
continue;
|
|
95
|
-
endpoints.push({ pointId: pid, x: pt.x, y: pt.y });
|
|
99
|
+
endpoints.push({ pointId: pid, laneId: lane.id, side, x: pt.x, y: pt.y });
|
|
96
100
|
}
|
|
97
101
|
};
|
|
98
102
|
// Restrict to lanes that participate in a next/prev relationship.
|
|
@@ -104,22 +108,37 @@ function buildBoundaryAlignmentOverrides(shapeMap, lanes, epsilonPx = 30) {
|
|
|
104
108
|
if (hasPrev)
|
|
105
109
|
collectEndpoints(lane, 'start');
|
|
106
110
|
}
|
|
107
|
-
// Greedy clustering: group points within epsilon of each other
|
|
111
|
+
// Greedy clustering: group points within epsilon of each other, with two
|
|
112
|
+
// refinements over plain first-fit grouping:
|
|
113
|
+
// 1. An endpoint never joins a cluster that already holds the opposite end
|
|
114
|
+
// of the same lane. A connecting lane shorter than epsilon would
|
|
115
|
+
// otherwise get its start and end merged into one cluster, collapsing
|
|
116
|
+
// its centerline below the degenerate-road export guard and silently
|
|
117
|
+
// dropping the lane (and its next/prev chain).
|
|
118
|
+
// 2. Among the eligible clusters the nearest one wins, so the far end of a
|
|
119
|
+
// short lane clusters with its true counterpart rather than with the
|
|
120
|
+
// first cluster found within epsilon.
|
|
108
121
|
const clusters = [];
|
|
109
122
|
const eps2 = epsilonPx * epsilonPx;
|
|
110
123
|
for (const ep of endpoints) {
|
|
111
|
-
let
|
|
124
|
+
let best = null;
|
|
125
|
+
let bestD2 = Infinity;
|
|
112
126
|
for (const cluster of clusters) {
|
|
113
127
|
const c0 = cluster[0];
|
|
114
128
|
const dx = ep.x - c0.x;
|
|
115
129
|
const dy = ep.y - c0.y;
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
130
|
+
const d2 = dx * dx + dy * dy;
|
|
131
|
+
if (d2 > eps2 || d2 >= bestD2)
|
|
132
|
+
continue;
|
|
133
|
+
const conflictsOwnLane = cluster.some((m) => m.laneId === ep.laneId && m.side !== ep.side);
|
|
134
|
+
if (conflictsOwnLane)
|
|
135
|
+
continue;
|
|
136
|
+
best = cluster;
|
|
137
|
+
bestD2 = d2;
|
|
121
138
|
}
|
|
122
|
-
if (
|
|
139
|
+
if (best)
|
|
140
|
+
best.push(ep);
|
|
141
|
+
else
|
|
123
142
|
clusters.push([ep]);
|
|
124
143
|
}
|
|
125
144
|
// Use each cluster centroid as the snapped position.
|
|
@@ -161,31 +180,58 @@ function emitPlanView(geom) {
|
|
|
161
180
|
lines.push(` </planView>`);
|
|
162
181
|
return lines.join('\n');
|
|
163
182
|
}
|
|
164
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Map a lanelet-style lane subtype to an OpenDRIVE lane type. The exact
|
|
185
|
+
* OpenDRIVE type wins when the lane carries `odr_type` (set by the OpenDRIVE
|
|
186
|
+
* importer), so imported maps round-trip their lane types.
|
|
187
|
+
*/
|
|
188
|
+
const LANELET_SUBTYPE_TO_ODR_TYPE = {
|
|
189
|
+
road: 'driving',
|
|
190
|
+
highway: 'driving',
|
|
191
|
+
play_street: 'driving',
|
|
192
|
+
emergency_lane: 'shoulder',
|
|
193
|
+
bus_lane: 'bus',
|
|
194
|
+
bicycle_lane: 'biking',
|
|
195
|
+
walkway: 'sidewalk',
|
|
196
|
+
shared_walkway: 'sidewalk',
|
|
197
|
+
stairs: 'sidewalk',
|
|
198
|
+
crosswalk: 'walking',
|
|
199
|
+
exit: 'exit',
|
|
200
|
+
};
|
|
201
|
+
function odrLaneTypeFor(lane) {
|
|
202
|
+
const attrs = lane.props.attributes ?? {};
|
|
203
|
+
if (attrs.odr_type)
|
|
204
|
+
return attrs.odr_type;
|
|
205
|
+
return LANELET_SUBTYPE_TO_ODR_TYPE[attrs.subtype ?? ''] ?? 'driving';
|
|
206
|
+
}
|
|
207
|
+
/** Road mark type for a boundary linestring (dashed subtype -> broken). */
|
|
208
|
+
function roadMarkTypeFor(shapeMap, boundaryId) {
|
|
209
|
+
if (!boundaryId)
|
|
210
|
+
return 'solid';
|
|
211
|
+
const ls = shapeMap.get(boundaryId);
|
|
212
|
+
return ls?.props?.attributes?.subtype === 'dashed' ? 'broken' : 'solid';
|
|
213
|
+
}
|
|
214
|
+
function emitLanes(geom, hasPrev, hasNext, laneType, centerMark, outerMark) {
|
|
165
215
|
const lines = [];
|
|
166
216
|
lines.push(` <lanes>`);
|
|
217
|
+
// The plan view follows the lane centerline; shifting the lane reference by
|
|
218
|
+
// +width/2 puts center lane 0 on the lane's left boundary, so a single
|
|
219
|
+
// right lane (-1) spans the full lane width. One drawtonomy lane therefore
|
|
220
|
+
// maps to exactly one OpenDRIVE driving lane (not two half-width lanes).
|
|
221
|
+
emitLaneOffsetEntries(geom, lines);
|
|
167
222
|
lines.push(` <laneSection s="0">`);
|
|
168
|
-
//
|
|
169
|
-
lines.push(` <left>`);
|
|
170
|
-
lines.push(` <lane id="1" type="driving" level="false">`);
|
|
171
|
-
emitLaneLink(lines, hasPrev, hasNext, 1);
|
|
172
|
-
emitHalfWidthEntries(geom, lines);
|
|
173
|
-
lines.push(` <roadMark sOffset="0" type="solid" weight="standard" color="white" width="0.13"/>`);
|
|
174
|
-
lines.push(` </lane>`);
|
|
175
|
-
lines.push(` </left>`);
|
|
176
|
-
// Center reference line.
|
|
223
|
+
// Center reference line (= left boundary after the laneOffset shift).
|
|
177
224
|
lines.push(` <center>`);
|
|
178
225
|
lines.push(` <lane id="0" type="none" level="false">`);
|
|
179
226
|
lines.push(` <link/>`);
|
|
180
|
-
lines.push(` <roadMark sOffset="0" type="
|
|
227
|
+
lines.push(` <roadMark sOffset="0" type="${centerMark}" weight="standard" color="white" width="0.13"/>`);
|
|
181
228
|
lines.push(` </lane>`);
|
|
182
229
|
lines.push(` </center>`);
|
|
183
|
-
// Right side: id=-1.
|
|
184
230
|
lines.push(` <right>`);
|
|
185
|
-
lines.push(` <lane id="-1" type="
|
|
231
|
+
lines.push(` <lane id="-1" type="${laneType}" level="false">`);
|
|
186
232
|
emitLaneLink(lines, hasPrev, hasNext, -1);
|
|
187
|
-
|
|
188
|
-
lines.push(` <roadMark sOffset="0" type="
|
|
233
|
+
emitWidthEntries(geom, lines);
|
|
234
|
+
lines.push(` <roadMark sOffset="0" type="${outerMark}" weight="standard" color="white" width="0.13"/>`);
|
|
189
235
|
lines.push(` </lane>`);
|
|
190
236
|
lines.push(` </right>`);
|
|
191
237
|
lines.push(` </laneSection>`);
|
|
@@ -206,28 +252,139 @@ function emitLaneLink(out, hasPrev, hasNext, laneId) {
|
|
|
206
252
|
}
|
|
207
253
|
out.push(` </link>`);
|
|
208
254
|
}
|
|
209
|
-
|
|
255
|
+
/** Piecewise-linear laneOffset records: +width/2 toward the left boundary. */
|
|
256
|
+
function emitLaneOffsetEntries(geom, out) {
|
|
210
257
|
for (let i = 0; i < geom.odrSamples.length - 1; i++) {
|
|
211
258
|
const s = geom.odrSamples[i].s;
|
|
212
259
|
const halfA = geom.odrSamples[i].width / 2;
|
|
213
260
|
const halfB = geom.odrSamples[i + 1].width / 2;
|
|
214
261
|
const segLen = geom.odrSamples[i + 1].s - s;
|
|
215
|
-
// Linear fit a + b*ds (c=d=0).
|
|
216
|
-
const a = halfA;
|
|
217
262
|
const b = segLen > 1e-9 ? (halfB - halfA) / segLen : 0;
|
|
218
|
-
out.push(`
|
|
263
|
+
out.push(` <laneOffset s="${fmt(s)}" a="${fmt(halfA)}" b="${fmt(b)}" c="0" d="0"/>`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Piecewise-linear full lane width records: a + b*ds (c=d=0). */
|
|
267
|
+
function emitWidthEntries(geom, out) {
|
|
268
|
+
for (let i = 0; i < geom.odrSamples.length - 1; i++) {
|
|
269
|
+
const s = geom.odrSamples[i].s;
|
|
270
|
+
const wA = geom.odrSamples[i].width;
|
|
271
|
+
const wB = geom.odrSamples[i + 1].width;
|
|
272
|
+
const segLen = geom.odrSamples[i + 1].s - s;
|
|
273
|
+
const b = segLen > 1e-9 ? (wB - wA) / segLen : 0;
|
|
274
|
+
out.push(` <width sOffset="${fmt(s)}" a="${fmt(wA)}" b="${fmt(b)}" c="0" d="0"/>`);
|
|
219
275
|
}
|
|
220
276
|
}
|
|
221
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Plan synthesized <junction> elements for branch / merge connectivity.
|
|
279
|
+
*
|
|
280
|
+
* A road <link> can name only one predecessor and one successor, so an edge
|
|
281
|
+
* is representable as plain road links only when it is its source's only
|
|
282
|
+
* `next` AND its target's only `prev`. Every other edge (branching or
|
|
283
|
+
* merging) is routed through a synthesized junction: the target roads become
|
|
284
|
+
* connecting roads (their <road junction> attribute is set) and each edge is
|
|
285
|
+
* emitted as a <connection incomingRoad connectingRoad contactPoint="start">
|
|
286
|
+
* carrying a <laneLink from="-1" to="-1"/> (the exporter emits exactly one
|
|
287
|
+
* driving lane -1 per road). Edges that share a lane collapse into the same
|
|
288
|
+
* junction (connected components), so a 2-in x 2-out diamond becomes one
|
|
289
|
+
* junction with four connections.
|
|
290
|
+
*/
|
|
291
|
+
function planJunctions(lanes, laneIdToRoadId, firstJunctionId) {
|
|
292
|
+
const validNext = new Map();
|
|
293
|
+
const validPrev = new Map();
|
|
294
|
+
for (const lane of lanes) {
|
|
295
|
+
if (!laneIdToRoadId.has(lane.id))
|
|
296
|
+
continue;
|
|
297
|
+
validNext.set(lane.id, (lane.props.next ?? []).filter((id) => laneIdToRoadId.has(id)));
|
|
298
|
+
validPrev.set(lane.id, (lane.props.prev ?? []).filter((id) => laneIdToRoadId.has(id)));
|
|
299
|
+
}
|
|
300
|
+
// Union-find over the lanes participating in junction-routed edges.
|
|
301
|
+
const parent = new Map();
|
|
302
|
+
const find = (x) => {
|
|
303
|
+
let root = x;
|
|
304
|
+
while (true) {
|
|
305
|
+
const p = parent.get(root);
|
|
306
|
+
if (p === undefined || p === root)
|
|
307
|
+
break;
|
|
308
|
+
root = p;
|
|
309
|
+
}
|
|
310
|
+
let cur = x;
|
|
311
|
+
while (cur !== root) {
|
|
312
|
+
const p = parent.get(cur);
|
|
313
|
+
parent.set(cur, root);
|
|
314
|
+
cur = p;
|
|
315
|
+
}
|
|
316
|
+
return root;
|
|
317
|
+
};
|
|
318
|
+
const union = (a, b) => {
|
|
319
|
+
if (!parent.has(a))
|
|
320
|
+
parent.set(a, a);
|
|
321
|
+
if (!parent.has(b))
|
|
322
|
+
parent.set(b, b);
|
|
323
|
+
const ra = find(a);
|
|
324
|
+
const rb = find(b);
|
|
325
|
+
if (ra !== rb)
|
|
326
|
+
parent.set(rb, ra);
|
|
327
|
+
};
|
|
328
|
+
const junctionEdges = [];
|
|
329
|
+
for (const [laneId, nexts] of validNext) {
|
|
330
|
+
for (const to of nexts) {
|
|
331
|
+
const prevsOfTarget = validPrev.get(to) ?? [];
|
|
332
|
+
if (nexts.length === 1 && prevsOfTarget.length === 1)
|
|
333
|
+
continue; // plain road link
|
|
334
|
+
junctionEdges.push({ from: laneId, to });
|
|
335
|
+
union(laneId, to);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const plan = {
|
|
339
|
+
nextJunction: new Map(),
|
|
340
|
+
prevJunction: new Map(),
|
|
341
|
+
roadJunction: new Map(),
|
|
342
|
+
junctions: [],
|
|
343
|
+
};
|
|
344
|
+
const junctionByRoot = new Map();
|
|
345
|
+
let nextId = firstJunctionId;
|
|
346
|
+
for (const e of junctionEdges) {
|
|
347
|
+
const root = find(e.from);
|
|
348
|
+
let junction = junctionByRoot.get(root);
|
|
349
|
+
if (!junction) {
|
|
350
|
+
junction = { id: nextId++, connections: [] };
|
|
351
|
+
junctionByRoot.set(root, junction);
|
|
352
|
+
plan.junctions.push(junction);
|
|
353
|
+
}
|
|
354
|
+
junction.connections.push({
|
|
355
|
+
incoming: laneIdToRoadId.get(e.from),
|
|
356
|
+
connecting: laneIdToRoadId.get(e.to),
|
|
357
|
+
});
|
|
358
|
+
plan.nextJunction.set(e.from, junction.id);
|
|
359
|
+
plan.prevJunction.set(e.to, junction.id);
|
|
360
|
+
plan.roadJunction.set(e.to, junction.id);
|
|
361
|
+
}
|
|
362
|
+
return plan;
|
|
363
|
+
}
|
|
364
|
+
function emitLink(lane, laneIdToRoadId, junctionPlan) {
|
|
222
365
|
const lines = [];
|
|
223
366
|
lines.push(` <link>`);
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
lines.push(` <predecessor elementType="
|
|
367
|
+
const prevJunction = junctionPlan.prevJunction.get(lane.id);
|
|
368
|
+
if (prevJunction !== undefined) {
|
|
369
|
+
lines.push(` <predecessor elementType="junction" elementId="${prevJunction}"/>`);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
const prevLane = (lane.props.prev ?? []).find((id) => laneIdToRoadId.has(id));
|
|
373
|
+
if (prevLane) {
|
|
374
|
+
const rid = laneIdToRoadId.get(prevLane);
|
|
375
|
+
lines.push(` <predecessor elementType="road" elementId="${rid}" contactPoint="end"/>`);
|
|
376
|
+
}
|
|
227
377
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
lines.push(` <successor elementType="
|
|
378
|
+
const nextJunction = junctionPlan.nextJunction.get(lane.id);
|
|
379
|
+
if (nextJunction !== undefined) {
|
|
380
|
+
lines.push(` <successor elementType="junction" elementId="${nextJunction}"/>`);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
const nextLane = (lane.props.next ?? []).find((id) => laneIdToRoadId.has(id));
|
|
384
|
+
if (nextLane) {
|
|
385
|
+
const rid = laneIdToRoadId.get(nextLane);
|
|
386
|
+
lines.push(` <successor elementType="road" elementId="${rid}" contactPoint="start"/>`);
|
|
387
|
+
}
|
|
231
388
|
}
|
|
232
389
|
lines.push(` </link>`);
|
|
233
390
|
return lines.join('\n');
|
|
@@ -276,9 +433,11 @@ function projectToRoad(geom, xG, yG) {
|
|
|
276
433
|
}
|
|
277
434
|
return { s: bestS, t: bestT, hdg: bestHdg, distance: bestDist, clampedAtEnd: bestClamped };
|
|
278
435
|
}
|
|
279
|
-
function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, laneIdToRoadId, maxAttachDistanceMeter = 50) {
|
|
436
|
+
function attachShapesToRoads(shapeMap, trafficLights, crosswalks, polygons, laneToGeom, laneIdToRoadId, maxAttachDistanceMeter = 50) {
|
|
280
437
|
const roadSignals = new Map();
|
|
281
438
|
const roadObjects = new Map();
|
|
439
|
+
const roadSignalRefs = new Map();
|
|
440
|
+
const signalIdByShape = new Map();
|
|
282
441
|
let signalIdCounter = 1;
|
|
283
442
|
let objectIdCounter = 1;
|
|
284
443
|
const roads = [];
|
|
@@ -287,16 +446,43 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
287
446
|
if (roadId !== undefined)
|
|
288
447
|
roads.push({ laneId, geom, roadId });
|
|
289
448
|
});
|
|
449
|
+
const geomByRoadId = new Map();
|
|
450
|
+
for (const r of roads)
|
|
451
|
+
geomByRoadId.set(r.roadId, r.geom);
|
|
290
452
|
for (const tl of trafficLights) {
|
|
291
453
|
const xG = pxToEnuX(tl.x);
|
|
292
454
|
const yG = pxToEnuY(tl.y);
|
|
455
|
+
// Regulatory layer: a signal that names affected lanes attaches to the
|
|
456
|
+
// nearest of those lanes' roads and carries a <validity> record for the
|
|
457
|
+
// road's single driving lane (-1). Distance gating does not apply — the
|
|
458
|
+
// assignment is explicit.
|
|
459
|
+
const affectedRoadIds = new Set();
|
|
460
|
+
for (const laneShapeId of tl.props.affectedLaneIds ?? []) {
|
|
461
|
+
const rid = laneIdToRoadId.get(laneShapeId);
|
|
462
|
+
if (rid !== undefined)
|
|
463
|
+
affectedRoadIds.add(rid);
|
|
464
|
+
}
|
|
293
465
|
let best = null;
|
|
294
|
-
|
|
295
|
-
const
|
|
296
|
-
|
|
297
|
-
|
|
466
|
+
if (affectedRoadIds.size > 0) {
|
|
467
|
+
for (const r of roads) {
|
|
468
|
+
if (!affectedRoadIds.has(r.roadId))
|
|
469
|
+
continue;
|
|
470
|
+
const proj = projectToRoad(r.geom, xG, yG);
|
|
471
|
+
if (!best || proj.distance < best.proj.distance)
|
|
472
|
+
best = { roadId: r.roadId, proj };
|
|
473
|
+
}
|
|
298
474
|
}
|
|
299
|
-
|
|
475
|
+
else {
|
|
476
|
+
// Fall back to the nearest road within the attachment distance.
|
|
477
|
+
for (const r of roads) {
|
|
478
|
+
const proj = projectToRoad(r.geom, xG, yG);
|
|
479
|
+
if (!best || proj.distance < best.proj.distance)
|
|
480
|
+
best = { roadId: r.roadId, proj };
|
|
481
|
+
}
|
|
482
|
+
if (best && best.proj.distance > maxAttachDistanceMeter)
|
|
483
|
+
best = null;
|
|
484
|
+
}
|
|
485
|
+
if (!best)
|
|
300
486
|
continue;
|
|
301
487
|
const style = tl.props.style ?? '';
|
|
302
488
|
const isPed = style.startsWith('pedestrian') || style.includes('ped');
|
|
@@ -305,7 +491,7 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
305
491
|
const heightM = pxToMeter(tl.props.h);
|
|
306
492
|
const widthM = pxToMeter(tl.props.w);
|
|
307
493
|
const list = roadSignals.get(best.roadId) ?? [];
|
|
308
|
-
|
|
494
|
+
const entry = {
|
|
309
495
|
id: signalIdCounter++,
|
|
310
496
|
s: best.proj.s,
|
|
311
497
|
t: best.proj.t,
|
|
@@ -317,8 +503,69 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
317
503
|
subtype: '-1',
|
|
318
504
|
dynamic: 'yes',
|
|
319
505
|
orientation: best.proj.t >= 0 ? '+' : '-',
|
|
320
|
-
}
|
|
506
|
+
};
|
|
507
|
+
if (affectedRoadIds.size > 0) {
|
|
508
|
+
// The exporter emits one driving lane (-1) per road.
|
|
509
|
+
entry.validity = { fromLane: -1, toLane: -1 };
|
|
510
|
+
}
|
|
511
|
+
list.push(entry);
|
|
321
512
|
roadSignals.set(best.roadId, list);
|
|
513
|
+
signalIdByShape.set(tl.id, entry.id);
|
|
514
|
+
// A signal controlling several lanes spans several roads in the
|
|
515
|
+
// 1-lane-per-road model. The remaining affected roads get a standard
|
|
516
|
+
// <signalReference> pointing back at the signal so the validity links
|
|
517
|
+
// survive a round trip (and ODR consumers see the full coverage).
|
|
518
|
+
if (affectedRoadIds.size > 1) {
|
|
519
|
+
for (const r of roads) {
|
|
520
|
+
if (r.roadId === best.roadId || !affectedRoadIds.has(r.roadId))
|
|
521
|
+
continue;
|
|
522
|
+
const proj = projectToRoad(r.geom, xG, yG);
|
|
523
|
+
const refs = roadSignalRefs.get(r.roadId) ?? [];
|
|
524
|
+
refs.push({
|
|
525
|
+
id: entry.id,
|
|
526
|
+
s: proj.s,
|
|
527
|
+
t: proj.t,
|
|
528
|
+
orientation: proj.t >= 0 ? '+' : '-',
|
|
529
|
+
});
|
|
530
|
+
roadSignalRefs.set(r.roadId, refs);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Stop line: emitted on the signal's road as a conventional
|
|
534
|
+
// <object name="StopLine"> at the projected station of the line's midpoint.
|
|
535
|
+
if (tl.props.stopLineId) {
|
|
536
|
+
const stopLs = shapeMap.get(tl.props.stopLineId);
|
|
537
|
+
const pts = stopLs
|
|
538
|
+
? collectPoints(shapeMap, stopLs.props.pointIds, false, new Map())
|
|
539
|
+
: [];
|
|
540
|
+
if (pts.length >= 2) {
|
|
541
|
+
// Carry the full polyline (ENU meters) on the signal so an importer
|
|
542
|
+
// can rebuild the stop-line linestring and re-link it to the signal.
|
|
543
|
+
entry.stopLinePoints = pts.map((p) => ({ x: pxToEnuX(p.x), y: pxToEnuY(p.y) }));
|
|
544
|
+
const a = pts[0];
|
|
545
|
+
const b = pts[pts.length - 1];
|
|
546
|
+
const midX = pxToEnuX((a.x + b.x) / 2);
|
|
547
|
+
const midY = pxToEnuY((a.y + b.y) / 2);
|
|
548
|
+
const geom = geomByRoadId.get(best.roadId);
|
|
549
|
+
const proj = projectToRoad(geom, midX, midY);
|
|
550
|
+
const objList = roadObjects.get(best.roadId) ?? [];
|
|
551
|
+
objList.push({
|
|
552
|
+
id: objectIdCounter++,
|
|
553
|
+
s: proj.s,
|
|
554
|
+
t: proj.t,
|
|
555
|
+
zOffset: 0,
|
|
556
|
+
// Stop lines lie across the road (like crosswalks): local hdg = π/2,
|
|
557
|
+
// length spanning the painted line, conventional 0.3 m paint width.
|
|
558
|
+
hdg: Math.PI / 2,
|
|
559
|
+
length: pxToMeter(Math.hypot(b.x - a.x, b.y - a.y)),
|
|
560
|
+
width: 0.3,
|
|
561
|
+
height: 0,
|
|
562
|
+
name: 'StopLine',
|
|
563
|
+
type: 'none',
|
|
564
|
+
orientation: 'none',
|
|
565
|
+
});
|
|
566
|
+
roadObjects.set(best.roadId, objList);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
322
569
|
}
|
|
323
570
|
for (const cw of crosswalks) {
|
|
324
571
|
const rotDeg = cw.rotation || 0;
|
|
@@ -331,13 +578,35 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
331
578
|
const cyGlobal = cw.y + cyLocal;
|
|
332
579
|
const xG = pxToEnuX(cxGlobal);
|
|
333
580
|
const yG = pxToEnuY(cyGlobal);
|
|
581
|
+
// Regulatory layer: a crosswalk that names affected lanes attaches to the
|
|
582
|
+
// nearest of those lanes' roads (no distance gate — the assignment is
|
|
583
|
+
// explicit), mirroring the signal behavior above.
|
|
584
|
+
const affectedRoadIds = new Set();
|
|
585
|
+
for (const laneShapeId of cw.props.affectedLaneIds ?? []) {
|
|
586
|
+
const rid = laneIdToRoadId.get(laneShapeId);
|
|
587
|
+
if (rid !== undefined)
|
|
588
|
+
affectedRoadIds.add(rid);
|
|
589
|
+
}
|
|
334
590
|
let best = null;
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
591
|
+
if (affectedRoadIds.size > 0) {
|
|
592
|
+
for (const r of roads) {
|
|
593
|
+
if (!affectedRoadIds.has(r.roadId))
|
|
594
|
+
continue;
|
|
595
|
+
const proj = projectToRoad(r.geom, xG, yG);
|
|
596
|
+
if (!best || proj.distance < best.proj.distance)
|
|
597
|
+
best = { roadId: r.roadId, proj };
|
|
598
|
+
}
|
|
339
599
|
}
|
|
340
|
-
|
|
600
|
+
else {
|
|
601
|
+
for (const r of roads) {
|
|
602
|
+
const proj = projectToRoad(r.geom, xG, yG);
|
|
603
|
+
if (!best || proj.distance < best.proj.distance)
|
|
604
|
+
best = { roadId: r.roadId, proj };
|
|
605
|
+
}
|
|
606
|
+
if (best && best.proj.distance > maxAttachDistanceMeter)
|
|
607
|
+
best = null;
|
|
608
|
+
}
|
|
609
|
+
if (!best)
|
|
341
610
|
continue;
|
|
342
611
|
const dxLocal = cw.props.endX - cw.props.startX;
|
|
343
612
|
const dyLocal = cw.props.endY - cw.props.startY;
|
|
@@ -359,6 +628,23 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
359
628
|
// π/2 regardless of the user-drawn axis direction.
|
|
360
629
|
void relativeHdg;
|
|
361
630
|
const crosswalkHdg = Math.PI / 2;
|
|
631
|
+
// Regulatory links (affected roads + stop line polyline) ride along as
|
|
632
|
+
// <userData> — OpenDRIVE's standard extension mechanism — so they survive
|
|
633
|
+
// an .xodr round trip. Coordinates are ENU meters.
|
|
634
|
+
const userData = [];
|
|
635
|
+
if (affectedRoadIds.size > 0) {
|
|
636
|
+
const links = {
|
|
637
|
+
affectedRoads: [...affectedRoadIds].sort((a, b) => a - b).map(String),
|
|
638
|
+
};
|
|
639
|
+
if (cw.props.stopLineId) {
|
|
640
|
+
const stopLs = shapeMap.get(cw.props.stopLineId);
|
|
641
|
+
const pts = stopLs ? collectPoints(shapeMap, stopLs.props.pointIds, false, new Map()) : [];
|
|
642
|
+
if (pts.length >= 2) {
|
|
643
|
+
links.stopLine = pts.map((p) => [roundMm(pxToEnuX(p.x)), roundMm(pxToEnuY(p.y))]);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
userData.push({ code: 'crosswalkLinks', value: JSON.stringify(links) });
|
|
647
|
+
}
|
|
362
648
|
list.push({
|
|
363
649
|
id: objectIdCounter++,
|
|
364
650
|
s: best.proj.s,
|
|
@@ -371,6 +657,7 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
371
657
|
name: 'crosswalk',
|
|
372
658
|
type: 'crosswalk',
|
|
373
659
|
orientation: 'none',
|
|
660
|
+
userData: userData.length ? userData : undefined,
|
|
374
661
|
});
|
|
375
662
|
roadObjects.set(best.roadId, list);
|
|
376
663
|
}
|
|
@@ -445,19 +732,42 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
445
732
|
});
|
|
446
733
|
roadObjects.set(best.roadId, list);
|
|
447
734
|
}
|
|
448
|
-
return { roadSignals, roadObjects };
|
|
735
|
+
return { roadSignals, roadObjects, roadSignalRefs, signalIdByShape };
|
|
449
736
|
}
|
|
450
|
-
function emitSignals(signals) {
|
|
451
|
-
if (!signals.length)
|
|
737
|
+
function emitSignals(signals, references) {
|
|
738
|
+
if (!signals.length && !references.length)
|
|
452
739
|
return ` <signals/>`;
|
|
453
740
|
const lines = [];
|
|
454
741
|
lines.push(` <signals>`);
|
|
455
742
|
for (const s of signals) {
|
|
456
|
-
|
|
743
|
+
const attrs = `id="${s.id}" s="${fmt(s.s)}" t="${fmt(s.t)}" zOffset="${fmt(s.zOffset)}" name="${escapeXml(s.name)}" dynamic="${s.dynamic}" orientation="${s.orientation}" type="${s.type}" subtype="${s.subtype}" country="OpenDRIVE" value="0" height="${fmt(s.height)}" width="${fmt(s.width)}"`;
|
|
744
|
+
if (s.validity || s.stopLinePoints) {
|
|
745
|
+
lines.push(` <signal ${attrs}>`);
|
|
746
|
+
if (s.validity) {
|
|
747
|
+
lines.push(` <validity fromLane="${s.validity.fromLane}" toLane="${s.validity.toLane}"/>`);
|
|
748
|
+
}
|
|
749
|
+
if (s.stopLinePoints) {
|
|
750
|
+
const json = JSON.stringify(s.stopLinePoints.map((p) => [roundMm(p.x), roundMm(p.y)]));
|
|
751
|
+
lines.push(` <userData code="stopLine" value="${escapeXml(json)}"/>`);
|
|
752
|
+
}
|
|
753
|
+
lines.push(` </signal>`);
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
lines.push(` <signal ${attrs}/>`);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
for (const ref of references) {
|
|
760
|
+
lines.push(` <signalReference s="${fmt(ref.s)}" t="${fmt(ref.t)}" id="${ref.id}" orientation="${ref.orientation}">`);
|
|
761
|
+
lines.push(` <validity fromLane="-1" toLane="-1"/>`);
|
|
762
|
+
lines.push(` </signalReference>`);
|
|
457
763
|
}
|
|
458
764
|
lines.push(` </signals>`);
|
|
459
765
|
return lines.join('\n');
|
|
460
766
|
}
|
|
767
|
+
/** Round to millimeter precision for compact embedded JSON. */
|
|
768
|
+
function roundMm(v) {
|
|
769
|
+
return Math.round(v * 1000) / 1000;
|
|
770
|
+
}
|
|
461
771
|
function emitObjects(objects) {
|
|
462
772
|
if (!objects.length)
|
|
463
773
|
return ` <objects/>`;
|
|
@@ -478,20 +788,89 @@ function emitObjects(objects) {
|
|
|
478
788
|
lines.push(` </object>`);
|
|
479
789
|
}
|
|
480
790
|
else {
|
|
481
|
-
|
|
791
|
+
const attrs = `id="${o.id}" s="${fmt(o.s)}" t="${fmt(o.t)}" zOffset="${fmt(o.zOffset)}" hdg="${fmt(o.hdg)}" name="${escapeXml(o.name)}" type="${o.type}" orientation="${o.orientation}" length="${fmt(o.length)}" width="${fmt(o.width)}" height="${fmt(o.height)}"`;
|
|
792
|
+
if (o.userData?.length) {
|
|
793
|
+
lines.push(` <object ${attrs}>`);
|
|
794
|
+
for (const ud of o.userData) {
|
|
795
|
+
lines.push(` <userData code="${escapeXml(ud.code)}" value="${escapeXml(ud.value)}"/>`);
|
|
796
|
+
}
|
|
797
|
+
lines.push(` </object>`);
|
|
798
|
+
}
|
|
799
|
+
else {
|
|
800
|
+
lines.push(` <object ${attrs}/>`);
|
|
801
|
+
}
|
|
482
802
|
}
|
|
483
803
|
}
|
|
484
804
|
lines.push(` </objects>`);
|
|
485
805
|
return lines.join('\n');
|
|
486
806
|
}
|
|
487
|
-
|
|
807
|
+
/**
|
|
808
|
+
* Lane attributes that have no OpenDRIVE representation (speed_limit only
|
|
809
|
+
* partially maps, one_way / turn_direction / location and custom tags not at
|
|
810
|
+
* all) are stashed verbatim as JSON in <userData code="laneAttributes"> —
|
|
811
|
+
* OpenDRIVE's standard extension mechanism — and restored by the importer.
|
|
812
|
+
* `odr_*` meta attributes are excluded: they are regenerated on import.
|
|
813
|
+
*/
|
|
814
|
+
function emitLaneAttributesUserData(lane) {
|
|
815
|
+
const stash = {};
|
|
816
|
+
for (const [k, v] of Object.entries(lane.props.attributes ?? {})) {
|
|
817
|
+
if (k === 'type' || k.startsWith('odr_'))
|
|
818
|
+
continue;
|
|
819
|
+
if (v === undefined || v === null || v === '')
|
|
820
|
+
continue;
|
|
821
|
+
stash[k] = String(v);
|
|
822
|
+
}
|
|
823
|
+
if (Object.keys(stash).length === 0)
|
|
824
|
+
return null;
|
|
825
|
+
return ` <userData code="laneAttributes" value="${escapeXml(JSON.stringify(stash))}"/>`;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Right-of-way links (`yieldLaneIds`) have no canonical OpenDRIVE road-level
|
|
829
|
+
* representation in this exporter (junction <priority> mapping is out of
|
|
830
|
+
* scope), so the yielding lanes' road ids are stashed in
|
|
831
|
+
* <userData code="yieldRoads"> and restored by the importer.
|
|
832
|
+
*/
|
|
833
|
+
function emitYieldRoadsUserData(lane, laneIdToRoadId) {
|
|
834
|
+
const yieldRoadIds = [];
|
|
835
|
+
for (const yieldShapeId of lane.props.yieldLaneIds ?? []) {
|
|
836
|
+
const rid = laneIdToRoadId.get(yieldShapeId);
|
|
837
|
+
if (rid !== undefined && !yieldRoadIds.includes(rid))
|
|
838
|
+
yieldRoadIds.push(rid);
|
|
839
|
+
}
|
|
840
|
+
if (yieldRoadIds.length === 0)
|
|
841
|
+
return null;
|
|
842
|
+
const json = JSON.stringify(yieldRoadIds.sort((a, b) => a - b).map(String));
|
|
843
|
+
return ` <userData code="yieldRoads" value="${escapeXml(json)}"/>`;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Road length attribute. The plan-view geometries are emitted with rounded
|
|
847
|
+
* (6-decimal) s/length values whose cumulative extent can exceed the exact
|
|
848
|
+
* road length by ~1e-6, which strict consumers flag as "s too large". Use the
|
|
849
|
+
* emitted extent plus a tiny pad so the length always covers the geometry.
|
|
850
|
+
*/
|
|
851
|
+
function emittedRoadLength(geom) {
|
|
852
|
+
let extent = geom.length;
|
|
853
|
+
for (let i = 0; i < geom.odrSamples.length - 1; i++) {
|
|
854
|
+
const a = geom.odrSamples[i];
|
|
855
|
+
const b = geom.odrSamples[i + 1];
|
|
856
|
+
const segLen = Math.hypot(b.x - a.x, b.y - a.y);
|
|
857
|
+
if (segLen < 1e-9)
|
|
858
|
+
continue;
|
|
859
|
+
const end = parseFloat(fmt(a.s)) + parseFloat(fmt(segLen));
|
|
860
|
+
if (end > extent)
|
|
861
|
+
extent = end;
|
|
862
|
+
}
|
|
863
|
+
return extent + 1e-4;
|
|
864
|
+
}
|
|
865
|
+
function emitRoad(lane, geom, roadId, laneIdToRoadId, junctionPlan, signals, signalRefs, objects, shapeMap) {
|
|
488
866
|
const speed = lane.props.attributes?.speed_limit;
|
|
489
867
|
const name = escapeXml(lane.props.attributes?.subtype || 'road');
|
|
490
|
-
const hasPrev =
|
|
491
|
-
const hasNext =
|
|
868
|
+
const hasPrev = (lane.props.prev ?? []).some((id) => laneIdToRoadId.has(id));
|
|
869
|
+
const hasNext = (lane.props.next ?? []).some((id) => laneIdToRoadId.has(id));
|
|
870
|
+
const junctionAttr = junctionPlan.roadJunction.get(lane.id) ?? -1;
|
|
492
871
|
const lines = [];
|
|
493
|
-
lines.push(` <road name="${name}" length="${fmt(geom
|
|
494
|
-
lines.push(emitLink(lane, laneIdToRoadId));
|
|
872
|
+
lines.push(` <road name="${name}" length="${fmt(emittedRoadLength(geom))}" id="${roadId}" junction="${junctionAttr}">`);
|
|
873
|
+
lines.push(emitLink(lane, laneIdToRoadId, junctionPlan));
|
|
495
874
|
if (speed) {
|
|
496
875
|
lines.push(` <type s="0" type="town">`);
|
|
497
876
|
lines.push(` <speed max="${escapeXml(speed)}" unit="km/h"/>`);
|
|
@@ -500,9 +879,15 @@ function emitRoad(lane, geom, roadId, laneIdToRoadId, signals, objects) {
|
|
|
500
879
|
lines.push(emitPlanView(geom));
|
|
501
880
|
lines.push(` <elevationProfile/>`);
|
|
502
881
|
lines.push(` <lateralProfile/>`);
|
|
503
|
-
lines.push(emitLanes(geom, hasPrev, hasNext));
|
|
882
|
+
lines.push(emitLanes(geom, hasPrev, hasNext, odrLaneTypeFor(lane), roadMarkTypeFor(shapeMap, lane.props.leftBoundaryId), roadMarkTypeFor(shapeMap, lane.props.rightBoundaryId)));
|
|
504
883
|
lines.push(emitObjects(objects));
|
|
505
|
-
lines.push(emitSignals(signals));
|
|
884
|
+
lines.push(emitSignals(signals, signalRefs));
|
|
885
|
+
const userData = emitLaneAttributesUserData(lane);
|
|
886
|
+
if (userData)
|
|
887
|
+
lines.push(userData);
|
|
888
|
+
const yieldUserData = emitYieldRoadsUserData(lane, laneIdToRoadId);
|
|
889
|
+
if (yieldUserData)
|
|
890
|
+
lines.push(yieldUserData);
|
|
506
891
|
lines.push(` </road>`);
|
|
507
892
|
return lines.join('\n');
|
|
508
893
|
}
|
|
@@ -535,9 +920,9 @@ export function exportToOpenDrive(snapshot) {
|
|
|
535
920
|
polygons.push({ shape: poly, vertices });
|
|
536
921
|
}
|
|
537
922
|
}
|
|
538
|
-
//
|
|
923
|
+
// Road ids are assigned after geometry construction so degenerate lanes
|
|
924
|
+
// (zero-length centerlines) neither emit empty roads nor occupy link ids.
|
|
539
925
|
const laneIdToRoadId = new Map();
|
|
540
|
-
lanes.forEach((lane, i) => laneIdToRoadId.set(lane.id, i + 1));
|
|
541
926
|
const dateStr = new Date().toISOString();
|
|
542
927
|
const bbox = computeEnuBoundingBox(shapeMap);
|
|
543
928
|
const geoRefProj = originToProjString(snapshot.origin);
|
|
@@ -558,18 +943,58 @@ export function exportToOpenDrive(snapshot) {
|
|
|
558
943
|
const laneToGeom = new Map();
|
|
559
944
|
for (const lane of lanes) {
|
|
560
945
|
const geom = buildRoadGeometry(shapeMap, lane, pointOverrides);
|
|
561
|
-
if (geom)
|
|
946
|
+
if (geom && geom.length >= 0.01 && geom.odrSamples.length >= 2) {
|
|
562
947
|
laneToGeom.set(lane.id, geom);
|
|
948
|
+
}
|
|
563
949
|
}
|
|
564
|
-
|
|
950
|
+
let nextRoadId = 1;
|
|
951
|
+
for (const lane of lanes) {
|
|
952
|
+
if (laneToGeom.has(lane.id))
|
|
953
|
+
laneIdToRoadId.set(lane.id, nextRoadId++);
|
|
954
|
+
}
|
|
955
|
+
const junctionPlan = planJunctions(lanes, laneIdToRoadId, nextRoadId);
|
|
956
|
+
const { roadSignals, roadObjects, roadSignalRefs, signalIdByShape } = attachShapesToRoads(shapeMap, trafficLights, crosswalks, polygons, laneToGeom, laneIdToRoadId);
|
|
565
957
|
for (const lane of lanes) {
|
|
566
958
|
const geom = laneToGeom.get(lane.id);
|
|
567
959
|
if (!geom)
|
|
568
960
|
continue;
|
|
569
961
|
const roadId = laneIdToRoadId.get(lane.id);
|
|
570
962
|
const signals = roadSignals.get(roadId) ?? [];
|
|
963
|
+
const signalRefs = roadSignalRefs.get(roadId) ?? [];
|
|
571
964
|
const objects = roadObjects.get(roadId) ?? [];
|
|
572
|
-
lines.push(emitRoad(lane, geom, roadId, laneIdToRoadId, signals, objects));
|
|
965
|
+
lines.push(emitRoad(lane, geom, roadId, laneIdToRoadId, junctionPlan, signals, signalRefs, objects, shapeMap));
|
|
966
|
+
}
|
|
967
|
+
// Signal groups: traffic lights sharing a controllerId (one intersection)
|
|
968
|
+
// become a <controller> listing their emitted signals as <control> records.
|
|
969
|
+
const controllerGroups = new Map();
|
|
970
|
+
for (const tl of trafficLights) {
|
|
971
|
+
const groupId = tl.props.controllerId;
|
|
972
|
+
if (!groupId)
|
|
973
|
+
continue;
|
|
974
|
+
const signalId = signalIdByShape.get(tl.id);
|
|
975
|
+
if (signalId === undefined)
|
|
976
|
+
continue;
|
|
977
|
+
const group = controllerGroups.get(groupId) ?? [];
|
|
978
|
+
group.push(signalId);
|
|
979
|
+
controllerGroups.set(groupId, group);
|
|
980
|
+
}
|
|
981
|
+
let controllerIdCounter = 1;
|
|
982
|
+
for (const [groupId, signalIds] of controllerGroups) {
|
|
983
|
+
lines.push(` <controller id="${controllerIdCounter++}" name="${escapeXml(groupId)}" sequence="0">`);
|
|
984
|
+
for (const signalId of signalIds) {
|
|
985
|
+
lines.push(` <control signalId="${signalId}" type="0"/>`);
|
|
986
|
+
}
|
|
987
|
+
lines.push(` </controller>`);
|
|
988
|
+
}
|
|
989
|
+
// Synthesized junctions for branch / merge connectivity (see planJunctions).
|
|
990
|
+
for (const junction of junctionPlan.junctions) {
|
|
991
|
+
lines.push(` <junction id="${junction.id}" name="junction${junction.id}">`);
|
|
992
|
+
junction.connections.forEach((conn, idx) => {
|
|
993
|
+
lines.push(` <connection id="${idx}" incomingRoad="${conn.incoming}" connectingRoad="${conn.connecting}" contactPoint="start">`);
|
|
994
|
+
lines.push(` <laneLink from="-1" to="-1"/>`);
|
|
995
|
+
lines.push(` </connection>`);
|
|
996
|
+
});
|
|
997
|
+
lines.push(` </junction>`);
|
|
573
998
|
}
|
|
574
999
|
lines.push(`</OpenDRIVE>`);
|
|
575
1000
|
return lines.join('\n');
|