@drawtonomy/sdk 0.7.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 +5 -1
- package/dist/exporter/index.d.ts.map +1 -1
- package/dist/exporter/index.js +4 -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 +547 -69
- 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 +36 -0
- package/dist/exporter/projection.d.ts.map +1 -0
- package/dist/exporter/projection.js +64 -0
- package/dist/exporter/projection.js.map +1 -0
- package/dist/types.d.ts +38 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,18 @@
|
|
|
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';
|
|
15
|
+
import { originToProjString } from './projection';
|
|
11
16
|
import { escapeXml, fmt, pxToEnuX, pxToEnuY, pxToMeter } from './units';
|
|
12
17
|
/** O(1) shape lookup by id. */
|
|
13
18
|
function buildShapeMap(shapes) {
|
|
@@ -91,7 +96,7 @@ function buildBoundaryAlignmentOverrides(shapeMap, lanes, epsilonPx = 30) {
|
|
|
91
96
|
const pt = shapeMap.get(pid);
|
|
92
97
|
if (!pt)
|
|
93
98
|
continue;
|
|
94
|
-
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 });
|
|
95
100
|
}
|
|
96
101
|
};
|
|
97
102
|
// Restrict to lanes that participate in a next/prev relationship.
|
|
@@ -103,22 +108,37 @@ function buildBoundaryAlignmentOverrides(shapeMap, lanes, epsilonPx = 30) {
|
|
|
103
108
|
if (hasPrev)
|
|
104
109
|
collectEndpoints(lane, 'start');
|
|
105
110
|
}
|
|
106
|
-
// 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.
|
|
107
121
|
const clusters = [];
|
|
108
122
|
const eps2 = epsilonPx * epsilonPx;
|
|
109
123
|
for (const ep of endpoints) {
|
|
110
|
-
let
|
|
124
|
+
let best = null;
|
|
125
|
+
let bestD2 = Infinity;
|
|
111
126
|
for (const cluster of clusters) {
|
|
112
127
|
const c0 = cluster[0];
|
|
113
128
|
const dx = ep.x - c0.x;
|
|
114
129
|
const dy = ep.y - c0.y;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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;
|
|
120
138
|
}
|
|
121
|
-
if (
|
|
139
|
+
if (best)
|
|
140
|
+
best.push(ep);
|
|
141
|
+
else
|
|
122
142
|
clusters.push([ep]);
|
|
123
143
|
}
|
|
124
144
|
// Use each cluster centroid as the snapped position.
|
|
@@ -160,31 +180,58 @@ function emitPlanView(geom) {
|
|
|
160
180
|
lines.push(` </planView>`);
|
|
161
181
|
return lines.join('\n');
|
|
162
182
|
}
|
|
163
|
-
|
|
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) {
|
|
164
215
|
const lines = [];
|
|
165
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);
|
|
166
222
|
lines.push(` <laneSection s="0">`);
|
|
167
|
-
//
|
|
168
|
-
lines.push(` <left>`);
|
|
169
|
-
lines.push(` <lane id="1" type="driving" level="false">`);
|
|
170
|
-
emitLaneLink(lines, hasPrev, hasNext, 1);
|
|
171
|
-
emitHalfWidthEntries(geom, lines);
|
|
172
|
-
lines.push(` <roadMark sOffset="0" type="solid" weight="standard" color="white" width="0.13"/>`);
|
|
173
|
-
lines.push(` </lane>`);
|
|
174
|
-
lines.push(` </left>`);
|
|
175
|
-
// Center reference line.
|
|
223
|
+
// Center reference line (= left boundary after the laneOffset shift).
|
|
176
224
|
lines.push(` <center>`);
|
|
177
225
|
lines.push(` <lane id="0" type="none" level="false">`);
|
|
178
226
|
lines.push(` <link/>`);
|
|
179
|
-
lines.push(` <roadMark sOffset="0" type="
|
|
227
|
+
lines.push(` <roadMark sOffset="0" type="${centerMark}" weight="standard" color="white" width="0.13"/>`);
|
|
180
228
|
lines.push(` </lane>`);
|
|
181
229
|
lines.push(` </center>`);
|
|
182
|
-
// Right side: id=-1.
|
|
183
230
|
lines.push(` <right>`);
|
|
184
|
-
lines.push(` <lane id="-1" type="
|
|
231
|
+
lines.push(` <lane id="-1" type="${laneType}" level="false">`);
|
|
185
232
|
emitLaneLink(lines, hasPrev, hasNext, -1);
|
|
186
|
-
|
|
187
|
-
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"/>`);
|
|
188
235
|
lines.push(` </lane>`);
|
|
189
236
|
lines.push(` </right>`);
|
|
190
237
|
lines.push(` </laneSection>`);
|
|
@@ -205,28 +252,139 @@ function emitLaneLink(out, hasPrev, hasNext, laneId) {
|
|
|
205
252
|
}
|
|
206
253
|
out.push(` </link>`);
|
|
207
254
|
}
|
|
208
|
-
|
|
255
|
+
/** Piecewise-linear laneOffset records: +width/2 toward the left boundary. */
|
|
256
|
+
function emitLaneOffsetEntries(geom, out) {
|
|
209
257
|
for (let i = 0; i < geom.odrSamples.length - 1; i++) {
|
|
210
258
|
const s = geom.odrSamples[i].s;
|
|
211
259
|
const halfA = geom.odrSamples[i].width / 2;
|
|
212
260
|
const halfB = geom.odrSamples[i + 1].width / 2;
|
|
213
261
|
const segLen = geom.odrSamples[i + 1].s - s;
|
|
214
|
-
// Linear fit a + b*ds (c=d=0).
|
|
215
|
-
const a = halfA;
|
|
216
262
|
const b = segLen > 1e-9 ? (halfB - halfA) / segLen : 0;
|
|
217
|
-
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"/>`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
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);
|
|
218
361
|
}
|
|
362
|
+
return plan;
|
|
219
363
|
}
|
|
220
|
-
function emitLink(lane, laneIdToRoadId) {
|
|
364
|
+
function emitLink(lane, laneIdToRoadId, junctionPlan) {
|
|
221
365
|
const lines = [];
|
|
222
366
|
lines.push(` <link>`);
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
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
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const nextJunction = junctionPlan.nextJunction.get(lane.id);
|
|
379
|
+
if (nextJunction !== undefined) {
|
|
380
|
+
lines.push(` <successor elementType="junction" elementId="${nextJunction}"/>`);
|
|
226
381
|
}
|
|
227
|
-
|
|
228
|
-
const
|
|
229
|
-
|
|
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
|
+
}
|
|
230
388
|
}
|
|
231
389
|
lines.push(` </link>`);
|
|
232
390
|
return lines.join('\n');
|
|
@@ -275,9 +433,11 @@ function projectToRoad(geom, xG, yG) {
|
|
|
275
433
|
}
|
|
276
434
|
return { s: bestS, t: bestT, hdg: bestHdg, distance: bestDist, clampedAtEnd: bestClamped };
|
|
277
435
|
}
|
|
278
|
-
function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, laneIdToRoadId, maxAttachDistanceMeter = 50) {
|
|
436
|
+
function attachShapesToRoads(shapeMap, trafficLights, crosswalks, polygons, laneToGeom, laneIdToRoadId, maxAttachDistanceMeter = 50) {
|
|
279
437
|
const roadSignals = new Map();
|
|
280
438
|
const roadObjects = new Map();
|
|
439
|
+
const roadSignalRefs = new Map();
|
|
440
|
+
const signalIdByShape = new Map();
|
|
281
441
|
let signalIdCounter = 1;
|
|
282
442
|
let objectIdCounter = 1;
|
|
283
443
|
const roads = [];
|
|
@@ -286,16 +446,43 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
286
446
|
if (roadId !== undefined)
|
|
287
447
|
roads.push({ laneId, geom, roadId });
|
|
288
448
|
});
|
|
449
|
+
const geomByRoadId = new Map();
|
|
450
|
+
for (const r of roads)
|
|
451
|
+
geomByRoadId.set(r.roadId, r.geom);
|
|
289
452
|
for (const tl of trafficLights) {
|
|
290
453
|
const xG = pxToEnuX(tl.x);
|
|
291
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
|
+
}
|
|
292
465
|
let best = null;
|
|
293
|
-
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
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
|
+
}
|
|
297
474
|
}
|
|
298
|
-
|
|
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)
|
|
299
486
|
continue;
|
|
300
487
|
const style = tl.props.style ?? '';
|
|
301
488
|
const isPed = style.startsWith('pedestrian') || style.includes('ped');
|
|
@@ -304,7 +491,7 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
304
491
|
const heightM = pxToMeter(tl.props.h);
|
|
305
492
|
const widthM = pxToMeter(tl.props.w);
|
|
306
493
|
const list = roadSignals.get(best.roadId) ?? [];
|
|
307
|
-
|
|
494
|
+
const entry = {
|
|
308
495
|
id: signalIdCounter++,
|
|
309
496
|
s: best.proj.s,
|
|
310
497
|
t: best.proj.t,
|
|
@@ -316,8 +503,69 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
316
503
|
subtype: '-1',
|
|
317
504
|
dynamic: 'yes',
|
|
318
505
|
orientation: best.proj.t >= 0 ? '+' : '-',
|
|
319
|
-
}
|
|
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);
|
|
320
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
|
+
}
|
|
321
569
|
}
|
|
322
570
|
for (const cw of crosswalks) {
|
|
323
571
|
const rotDeg = cw.rotation || 0;
|
|
@@ -330,13 +578,35 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
330
578
|
const cyGlobal = cw.y + cyLocal;
|
|
331
579
|
const xG = pxToEnuX(cxGlobal);
|
|
332
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
|
+
}
|
|
333
590
|
let best = null;
|
|
334
|
-
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
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
|
+
}
|
|
338
599
|
}
|
|
339
|
-
|
|
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)
|
|
340
610
|
continue;
|
|
341
611
|
const dxLocal = cw.props.endX - cw.props.startX;
|
|
342
612
|
const dyLocal = cw.props.endY - cw.props.startY;
|
|
@@ -358,6 +628,23 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
358
628
|
// π/2 regardless of the user-drawn axis direction.
|
|
359
629
|
void relativeHdg;
|
|
360
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
|
+
}
|
|
361
648
|
list.push({
|
|
362
649
|
id: objectIdCounter++,
|
|
363
650
|
s: best.proj.s,
|
|
@@ -370,6 +657,7 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
370
657
|
name: 'crosswalk',
|
|
371
658
|
type: 'crosswalk',
|
|
372
659
|
orientation: 'none',
|
|
660
|
+
userData: userData.length ? userData : undefined,
|
|
373
661
|
});
|
|
374
662
|
roadObjects.set(best.roadId, list);
|
|
375
663
|
}
|
|
@@ -444,19 +732,42 @@ function attachShapesToRoads(trafficLights, crosswalks, polygons, laneToGeom, la
|
|
|
444
732
|
});
|
|
445
733
|
roadObjects.set(best.roadId, list);
|
|
446
734
|
}
|
|
447
|
-
return { roadSignals, roadObjects };
|
|
735
|
+
return { roadSignals, roadObjects, roadSignalRefs, signalIdByShape };
|
|
448
736
|
}
|
|
449
|
-
function emitSignals(signals) {
|
|
450
|
-
if (!signals.length)
|
|
737
|
+
function emitSignals(signals, references) {
|
|
738
|
+
if (!signals.length && !references.length)
|
|
451
739
|
return ` <signals/>`;
|
|
452
740
|
const lines = [];
|
|
453
741
|
lines.push(` <signals>`);
|
|
454
742
|
for (const s of signals) {
|
|
455
|
-
|
|
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>`);
|
|
456
763
|
}
|
|
457
764
|
lines.push(` </signals>`);
|
|
458
765
|
return lines.join('\n');
|
|
459
766
|
}
|
|
767
|
+
/** Round to millimeter precision for compact embedded JSON. */
|
|
768
|
+
function roundMm(v) {
|
|
769
|
+
return Math.round(v * 1000) / 1000;
|
|
770
|
+
}
|
|
460
771
|
function emitObjects(objects) {
|
|
461
772
|
if (!objects.length)
|
|
462
773
|
return ` <objects/>`;
|
|
@@ -477,20 +788,89 @@ function emitObjects(objects) {
|
|
|
477
788
|
lines.push(` </object>`);
|
|
478
789
|
}
|
|
479
790
|
else {
|
|
480
|
-
|
|
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
|
+
}
|
|
481
802
|
}
|
|
482
803
|
}
|
|
483
804
|
lines.push(` </objects>`);
|
|
484
805
|
return lines.join('\n');
|
|
485
806
|
}
|
|
486
|
-
|
|
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) {
|
|
487
866
|
const speed = lane.props.attributes?.speed_limit;
|
|
488
867
|
const name = escapeXml(lane.props.attributes?.subtype || 'road');
|
|
489
|
-
const hasPrev =
|
|
490
|
-
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;
|
|
491
871
|
const lines = [];
|
|
492
|
-
lines.push(` <road name="${name}" length="${fmt(geom
|
|
493
|
-
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));
|
|
494
874
|
if (speed) {
|
|
495
875
|
lines.push(` <type s="0" type="town">`);
|
|
496
876
|
lines.push(` <speed max="${escapeXml(speed)}" unit="km/h"/>`);
|
|
@@ -499,9 +879,15 @@ function emitRoad(lane, geom, roadId, laneIdToRoadId, signals, objects) {
|
|
|
499
879
|
lines.push(emitPlanView(geom));
|
|
500
880
|
lines.push(` <elevationProfile/>`);
|
|
501
881
|
lines.push(` <lateralProfile/>`);
|
|
502
|
-
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)));
|
|
503
883
|
lines.push(emitObjects(objects));
|
|
504
|
-
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);
|
|
505
891
|
lines.push(` </road>`);
|
|
506
892
|
return lines.join('\n');
|
|
507
893
|
}
|
|
@@ -534,32 +920,124 @@ export function exportToOpenDrive(snapshot) {
|
|
|
534
920
|
polygons.push({ shape: poly, vertices });
|
|
535
921
|
}
|
|
536
922
|
}
|
|
537
|
-
//
|
|
923
|
+
// Road ids are assigned after geometry construction so degenerate lanes
|
|
924
|
+
// (zero-length centerlines) neither emit empty roads nor occupy link ids.
|
|
538
925
|
const laneIdToRoadId = new Map();
|
|
539
|
-
lanes.forEach((lane, i) => laneIdToRoadId.set(lane.id, i + 1));
|
|
540
926
|
const dateStr = new Date().toISOString();
|
|
927
|
+
const bbox = computeEnuBoundingBox(shapeMap);
|
|
928
|
+
const geoRefProj = originToProjString(snapshot.origin);
|
|
541
929
|
const lines = [];
|
|
542
930
|
lines.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
543
931
|
lines.push(`<OpenDRIVE>`);
|
|
544
|
-
|
|
932
|
+
// OpenDRIVE 1.8 expects <geoReference> inside <header>. We always emit one —
|
|
933
|
+
// tmerc-at-origin when snapshot.origin is set, WGS84 longlat as a fallback —
|
|
934
|
+
// so downstream tools (esmini, RoadRunner, asam-qc-opendrive) see a defined
|
|
935
|
+
// coordinate reference system rather than nothing. The N/S/E/W attributes
|
|
936
|
+
// are populated from the actual point cloud so the header bbox reflects the
|
|
937
|
+
// map extent in ENU metres.
|
|
938
|
+
lines.push(` <header revMajor="1" revMinor="8" name="drawtonomy" version="1.0" date="${dateStr}" ` +
|
|
939
|
+
`north="${fmt(bbox.north)}" south="${fmt(bbox.south)}" east="${fmt(bbox.east)}" west="${fmt(bbox.west)}" vendor="drawtonomy">`);
|
|
940
|
+
lines.push(` <geoReference><![CDATA[${escapeCdata(geoRefProj)}]]></geoReference>`);
|
|
941
|
+
lines.push(` </header>`);
|
|
545
942
|
const pointOverrides = buildBoundaryAlignmentOverrides(shapeMap, lanes);
|
|
546
943
|
const laneToGeom = new Map();
|
|
547
944
|
for (const lane of lanes) {
|
|
548
945
|
const geom = buildRoadGeometry(shapeMap, lane, pointOverrides);
|
|
549
|
-
if (geom)
|
|
946
|
+
if (geom && geom.length >= 0.01 && geom.odrSamples.length >= 2) {
|
|
550
947
|
laneToGeom.set(lane.id, geom);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
let nextRoadId = 1;
|
|
951
|
+
for (const lane of lanes) {
|
|
952
|
+
if (laneToGeom.has(lane.id))
|
|
953
|
+
laneIdToRoadId.set(lane.id, nextRoadId++);
|
|
551
954
|
}
|
|
552
|
-
const
|
|
955
|
+
const junctionPlan = planJunctions(lanes, laneIdToRoadId, nextRoadId);
|
|
956
|
+
const { roadSignals, roadObjects, roadSignalRefs, signalIdByShape } = attachShapesToRoads(shapeMap, trafficLights, crosswalks, polygons, laneToGeom, laneIdToRoadId);
|
|
553
957
|
for (const lane of lanes) {
|
|
554
958
|
const geom = laneToGeom.get(lane.id);
|
|
555
959
|
if (!geom)
|
|
556
960
|
continue;
|
|
557
961
|
const roadId = laneIdToRoadId.get(lane.id);
|
|
558
962
|
const signals = roadSignals.get(roadId) ?? [];
|
|
963
|
+
const signalRefs = roadSignalRefs.get(roadId) ?? [];
|
|
559
964
|
const objects = roadObjects.get(roadId) ?? [];
|
|
560
|
-
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>`);
|
|
561
998
|
}
|
|
562
999
|
lines.push(`</OpenDRIVE>`);
|
|
563
1000
|
return lines.join('\n');
|
|
564
1001
|
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Compute the axis-aligned bounding box of all point shapes in ENU metres.
|
|
1004
|
+
* Used to populate OpenDRIVE <header> north/south/east/west attributes.
|
|
1005
|
+
* Returns zeros when the snapshot has no points.
|
|
1006
|
+
*/
|
|
1007
|
+
function computeEnuBoundingBox(shapeMap) {
|
|
1008
|
+
let minX = Infinity;
|
|
1009
|
+
let maxX = -Infinity;
|
|
1010
|
+
let minY = Infinity;
|
|
1011
|
+
let maxY = -Infinity;
|
|
1012
|
+
for (const s of shapeMap.values()) {
|
|
1013
|
+
if (s.type !== 'point')
|
|
1014
|
+
continue;
|
|
1015
|
+
if (s.x < minX)
|
|
1016
|
+
minX = s.x;
|
|
1017
|
+
if (s.x > maxX)
|
|
1018
|
+
maxX = s.x;
|
|
1019
|
+
if (s.y < minY)
|
|
1020
|
+
minY = s.y;
|
|
1021
|
+
if (s.y > maxY)
|
|
1022
|
+
maxY = s.y;
|
|
1023
|
+
}
|
|
1024
|
+
if (!Number.isFinite(minX)) {
|
|
1025
|
+
return { north: 0, south: 0, east: 0, west: 0 };
|
|
1026
|
+
}
|
|
1027
|
+
// Canvas y points down, ENU y points up — flip when reporting bounds.
|
|
1028
|
+
return {
|
|
1029
|
+
west: pxToEnuX(minX),
|
|
1030
|
+
east: pxToEnuX(maxX),
|
|
1031
|
+
south: pxToEnuY(maxY),
|
|
1032
|
+
north: pxToEnuY(minY),
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Escape a string so it can appear safely inside an XML CDATA section. The
|
|
1037
|
+
* only character sequence that ends a CDATA section is `]]>`, so we split it
|
|
1038
|
+
* across two CDATA sections.
|
|
1039
|
+
*/
|
|
1040
|
+
function escapeCdata(s) {
|
|
1041
|
+
return s.replace(/]]>/g, ']]]]><![CDATA[>');
|
|
1042
|
+
}
|
|
565
1043
|
//# sourceMappingURL=opendrive.js.map
|