@drawtonomy/sdk 0.9.0 → 0.11.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 -4
- package/dist/exporter/index.d.ts.map +1 -1
- package/dist/exporter/index.js +1 -1
- package/dist/exporter/index.js.map +1 -1
- package/dist/exporter/lanelet2.d.ts +12 -0
- package/dist/exporter/lanelet2.d.ts.map +1 -1
- package/dist/exporter/lanelet2.js +112 -31
- package/dist/exporter/lanelet2.js.map +1 -1
- package/dist/exporter/odrCarryThrough.d.ts +89 -0
- package/dist/exporter/odrCarryThrough.d.ts.map +1 -0
- package/dist/exporter/odrCarryThrough.js +186 -0
- package/dist/exporter/odrCarryThrough.js.map +1 -0
- package/dist/exporter/odrGeometryFit.d.ts +37 -0
- package/dist/exporter/odrGeometryFit.d.ts.map +1 -0
- package/dist/exporter/odrGeometryFit.js +476 -0
- package/dist/exporter/odrGeometryFit.js.map +1 -0
- package/dist/exporter/odrToShapes.d.ts +8 -0
- package/dist/exporter/odrToShapes.d.ts.map +1 -1
- package/dist/exporter/odrToShapes.js +444 -54
- package/dist/exporter/odrToShapes.js.map +1 -1
- package/dist/exporter/opendrive.d.ts +14 -1
- package/dist/exporter/opendrive.d.ts.map +1 -1
- package/dist/exporter/opendrive.js +1603 -315
- package/dist/exporter/opendrive.js.map +1 -1
- package/dist/exporter/opendriveParser.d.ts +12 -0
- package/dist/exporter/opendriveParser.d.ts.map +1 -1
- package/dist/exporter/opendriveParser.js +7 -2
- package/dist/exporter/opendriveParser.js.map +1 -1
- package/dist/exporter/osmToShapes.d.ts +26 -1
- package/dist/exporter/osmToShapes.d.ts.map +1 -1
- package/dist/exporter/osmToShapes.js +54 -9
- package/dist/exporter/osmToShapes.js.map +1 -1
- package/dist/exporter/units.d.ts +7 -0
- package/dist/exporter/units.d.ts.map +1 -1
- package/dist/exporter/units.js +11 -0
- package/dist/exporter/units.js.map +1 -1
- package/dist/types.d.ts +28 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Convert a parsed OpenDRIVE model (`OdrMap`) into the shape primitives that
|
|
2
|
-
// the drawtonomy editor consumes (points, linestrings, lanes, traffic lights
|
|
2
|
+
// the drawtonomy editor consumes (points, linestrings, lanes, traffic lights,
|
|
3
|
+
// traffic signs, crosswalks).
|
|
3
4
|
//
|
|
4
5
|
// Mirrors the Lanelet2 OSM import path (`osmToShapes`): the output is the
|
|
5
6
|
// same intermediate `ImportedShapes` structure, extended with a sidecar (the
|
|
@@ -29,6 +30,7 @@ import { evalPoly3, sampleReferenceLine } from './odrGeometry';
|
|
|
29
30
|
import { sampleAtParam } from './laneCenterline';
|
|
30
31
|
import { createShapeIdAllocator, } from './osmToShapes';
|
|
31
32
|
import { PIXELS_PER_METER } from './units';
|
|
33
|
+
import { hashRoadState, } from './odrCarryThrough';
|
|
32
34
|
/** Lanes narrower than this (m) carry no usable area and are skipped. */
|
|
33
35
|
const WIDTH_EPS = 1e-3;
|
|
34
36
|
const S_EPS = 1e-6;
|
|
@@ -219,6 +221,7 @@ export function odrToShapes(map, options = {}) {
|
|
|
219
221
|
linestrings: [],
|
|
220
222
|
lanes: [],
|
|
221
223
|
trafficLights: [],
|
|
224
|
+
trafficSigns: [],
|
|
222
225
|
crosswalks: [],
|
|
223
226
|
bounds: emptyBounds(),
|
|
224
227
|
sidecar: {
|
|
@@ -255,35 +258,52 @@ export function odrToShapes(map, options = {}) {
|
|
|
255
258
|
const roadById = new Map();
|
|
256
259
|
/** Roads (with their reference samples) whose signals are converted after all lanes exist. */
|
|
257
260
|
const signalRoads = [];
|
|
258
|
-
/** Lane attributes restored from <userData code="laneAttributes"> per road. */
|
|
259
261
|
const restoredAttrCache = new Map();
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
262
|
+
const pickStringAttrs = (obj) => {
|
|
263
|
+
const out = {};
|
|
264
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
265
|
+
// `type` is fixed to 'lanelet' and odr_* meta is regenerated.
|
|
266
|
+
if (typeof v !== 'string' || k === 'type' || k.startsWith('odr_'))
|
|
267
|
+
continue;
|
|
268
|
+
out[k] = v;
|
|
269
|
+
}
|
|
270
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
271
|
+
};
|
|
272
|
+
const restoredLaneAttributes = (road, odrLaneId) => {
|
|
273
|
+
let entry = restoredAttrCache.get(road.id);
|
|
274
|
+
if (entry === undefined) {
|
|
275
|
+
entry = { flat: null, perLane: null };
|
|
276
|
+
const raw = road.userData['laneAttributes'];
|
|
277
|
+
if (raw) {
|
|
278
|
+
try {
|
|
279
|
+
const obj = JSON.parse(raw);
|
|
280
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
281
|
+
const flat = {};
|
|
282
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
283
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && /^-?\d+$/.test(k)) {
|
|
284
|
+
const laneAttrs = pickStringAttrs(v);
|
|
285
|
+
if (laneAttrs) {
|
|
286
|
+
entry.perLane = entry.perLane ?? new Map();
|
|
287
|
+
entry.perLane.set(parseInt(k, 10), laneAttrs);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
else {
|
|
291
|
+
flat[k] = v;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
entry.flat = pickStringAttrs(flat);
|
|
276
295
|
}
|
|
277
|
-
|
|
278
|
-
|
|
296
|
+
}
|
|
297
|
+
catch {
|
|
298
|
+
// Malformed userData JSON is ignored (third-party files).
|
|
279
299
|
}
|
|
280
300
|
}
|
|
281
|
-
|
|
282
|
-
// Malformed userData JSON is ignored (third-party files).
|
|
283
|
-
}
|
|
301
|
+
restoredAttrCache.set(road.id, entry);
|
|
284
302
|
}
|
|
285
|
-
|
|
286
|
-
|
|
303
|
+
const perLane = entry.perLane?.get(odrLaneId) ?? null;
|
|
304
|
+
if (!entry.flat && !perLane)
|
|
305
|
+
return null;
|
|
306
|
+
return { ...(entry.flat ?? {}), ...(perLane ?? {}) };
|
|
287
307
|
};
|
|
288
308
|
const registryKey = (roadId, sectionIdx, odrLaneId) => `${roadId}|${sectionIdx}|${odrLaneId}`;
|
|
289
309
|
for (const road of roads) {
|
|
@@ -482,17 +502,22 @@ export function odrToShapes(map, options = {}) {
|
|
|
482
502
|
}
|
|
483
503
|
}
|
|
484
504
|
/**
|
|
485
|
-
* Convert
|
|
486
|
-
*
|
|
487
|
-
*
|
|
488
|
-
*
|
|
489
|
-
*
|
|
505
|
+
* Convert signals into shapes: type 1000001/1000002 become traffic lights,
|
|
506
|
+
* any other type with dynamic != "yes" becomes a static traffic sign. The
|
|
507
|
+
* position is the (s, t) station evaluated on the reference line;
|
|
508
|
+
* `affectedLaneIds` resolves the <validity> lane range against the lane
|
|
509
|
+
* section containing s (falling back to every driving lane of the road),
|
|
510
|
+
* merged with the lanes of any road that re-applies the signal via
|
|
490
511
|
* <signalReference>. A <userData code="stopLine"> record is rebuilt into a
|
|
491
|
-
* stop-line linestring and linked through `stopLineId
|
|
512
|
+
* stop-line linestring and linked through `stopLineId`; sign attributes
|
|
513
|
+
* stashed in <userData code="signAttributes"> (sign_code etc.) are restored.
|
|
492
514
|
*/
|
|
493
515
|
function materializeSignals(road, samples) {
|
|
494
516
|
for (const sig of road.signals) {
|
|
495
|
-
|
|
517
|
+
const isTrafficLight = TRAFFIC_LIGHT_SIGNAL_TYPES.has(sig.type);
|
|
518
|
+
// Dynamic signals of unknown type cannot be represented as static
|
|
519
|
+
// signs; they stay sidecar-only (counted in the warnings).
|
|
520
|
+
if (!isTrafficLight && sig.dynamic === 'yes')
|
|
496
521
|
continue;
|
|
497
522
|
const pose = poseAt(samples, sig.s);
|
|
498
523
|
// Unit normal toward +t (left of the reference direction in ENU).
|
|
@@ -505,25 +530,71 @@ export function odrToShapes(map, options = {}) {
|
|
|
505
530
|
continue;
|
|
506
531
|
resolveAffectedLanes(ref.road, ref.s, ref.validity, affected);
|
|
507
532
|
}
|
|
533
|
+
if (isTrafficLight) {
|
|
534
|
+
const data = {
|
|
535
|
+
id: idAllocator.next('traffic_light'),
|
|
536
|
+
x: enuToCanvasX(ex),
|
|
537
|
+
y: enuToCanvasY(ey),
|
|
538
|
+
// Default editor proportions (30x60 px) when the signal carries no size.
|
|
539
|
+
w: sig.width > 0 ? sig.width * PIXELS_PER_METER : 30,
|
|
540
|
+
h: sig.height > 0 ? sig.height * PIXELS_PER_METER : 60,
|
|
541
|
+
osmId: '',
|
|
542
|
+
affectedLaneIds: affected,
|
|
543
|
+
stopLineId: materializeStopLine(sig.userData['stopLine']),
|
|
544
|
+
attributes: {
|
|
545
|
+
type: 'traffic_light',
|
|
546
|
+
odr_signal_id: sig.id,
|
|
547
|
+
odr_road_id: road.id,
|
|
548
|
+
odr_signal_type: sig.type,
|
|
549
|
+
odr_signal_subtype: sig.subtype,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
result.trafficLights.push(data);
|
|
553
|
+
convertedSignalCount++;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
// Static traffic sign. Attributes stashed by the exporter in
|
|
557
|
+
// <userData code="signAttributes"> (sign_code, sign_type, regulatory
|
|
558
|
+
// element subtype, custom tags) are restored verbatim; third-party
|
|
559
|
+
// signs fall back to the signal name as the sign code.
|
|
560
|
+
const attributes = { type: 'traffic_sign' };
|
|
561
|
+
const rawSignAttrs = sig.userData['signAttributes'];
|
|
562
|
+
if (rawSignAttrs) {
|
|
563
|
+
try {
|
|
564
|
+
const obj = JSON.parse(rawSignAttrs);
|
|
565
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
566
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
567
|
+
if (typeof v !== 'string' || k === 'type' || k.startsWith('odr_'))
|
|
568
|
+
continue;
|
|
569
|
+
attributes[k] = v;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
// Malformed userData JSON is ignored (third-party files).
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (!attributes.sign_code && sig.name)
|
|
578
|
+
attributes.sign_code = sig.name;
|
|
579
|
+
attributes.odr_signal_id = sig.id;
|
|
580
|
+
attributes.odr_road_id = road.id;
|
|
581
|
+
attributes.odr_signal_type = sig.type;
|
|
582
|
+
attributes.odr_signal_subtype = sig.subtype;
|
|
583
|
+
if (sig.country)
|
|
584
|
+
attributes.odr_country = sig.country;
|
|
508
585
|
const data = {
|
|
509
|
-
id: idAllocator.next('
|
|
586
|
+
id: idAllocator.next('traffic_sign'),
|
|
510
587
|
x: enuToCanvasX(ex),
|
|
511
588
|
y: enuToCanvasY(ey),
|
|
512
|
-
// Default editor proportions (
|
|
589
|
+
// Default editor proportions (30x30 px) when the signal carries no size.
|
|
513
590
|
w: sig.width > 0 ? sig.width * PIXELS_PER_METER : 30,
|
|
514
|
-
h: sig.height > 0 ? sig.height * PIXELS_PER_METER :
|
|
591
|
+
h: sig.height > 0 ? sig.height * PIXELS_PER_METER : 30,
|
|
515
592
|
osmId: '',
|
|
516
593
|
affectedLaneIds: affected,
|
|
517
594
|
stopLineId: materializeStopLine(sig.userData['stopLine']),
|
|
518
|
-
attributes
|
|
519
|
-
type: 'traffic_light',
|
|
520
|
-
odr_signal_id: sig.id,
|
|
521
|
-
odr_road_id: road.id,
|
|
522
|
-
odr_signal_type: sig.type,
|
|
523
|
-
odr_signal_subtype: sig.subtype,
|
|
524
|
-
},
|
|
595
|
+
attributes,
|
|
525
596
|
};
|
|
526
|
-
result.
|
|
597
|
+
result.trafficSigns.push(data);
|
|
527
598
|
convertedSignalCount++;
|
|
528
599
|
}
|
|
529
600
|
}
|
|
@@ -539,6 +610,36 @@ export function odrToShapes(map, options = {}) {
|
|
|
539
610
|
}
|
|
540
611
|
return out;
|
|
541
612
|
};
|
|
613
|
+
/** Shape ids of a specific (road, ODR lane id) pair, across all sections. */
|
|
614
|
+
const lanesOfRoadLane = (roadId, odrLaneId) => {
|
|
615
|
+
const out = [];
|
|
616
|
+
for (const reg of lanesByRoad.get(roadId) ?? []) {
|
|
617
|
+
if (reg.odrLaneId === odrLaneId && !out.includes(reg.shapeId))
|
|
618
|
+
out.push(reg.shapeId);
|
|
619
|
+
}
|
|
620
|
+
return out;
|
|
621
|
+
};
|
|
622
|
+
/** Resolve a [[roadId, laneId], ...] JSON record to lane shape ids. */
|
|
623
|
+
const resolveLanePairs = (pairs) => {
|
|
624
|
+
const out = [];
|
|
625
|
+
if (!Array.isArray(pairs))
|
|
626
|
+
return out;
|
|
627
|
+
for (const entry of pairs) {
|
|
628
|
+
if (!Array.isArray(entry) || entry.length < 2)
|
|
629
|
+
continue;
|
|
630
|
+
const [rid, lid] = entry;
|
|
631
|
+
if (typeof rid !== 'string' || typeof lid !== 'string')
|
|
632
|
+
continue;
|
|
633
|
+
const odrLaneId = parseInt(lid, 10);
|
|
634
|
+
if (!Number.isFinite(odrLaneId))
|
|
635
|
+
continue;
|
|
636
|
+
for (const shapeId of lanesOfRoadLane(rid, odrLaneId)) {
|
|
637
|
+
if (!out.includes(shapeId))
|
|
638
|
+
out.push(shapeId);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
return out;
|
|
642
|
+
};
|
|
542
643
|
/**
|
|
543
644
|
* Convert crosswalk objects into crosswalk shapes. The band center is the
|
|
544
645
|
* (s, t) station on the reference line; the walking axis follows the
|
|
@@ -572,7 +673,15 @@ export function odrToShapes(map, options = {}) {
|
|
|
572
673
|
if (rawLinks) {
|
|
573
674
|
try {
|
|
574
675
|
const links = JSON.parse(rawLinks);
|
|
575
|
-
|
|
676
|
+
// Current exports carry lane-precise [[roadId, laneId], ...] pairs;
|
|
677
|
+
// legacy files only listed road ids (=> every driving lane).
|
|
678
|
+
if (Array.isArray(links.affectedLanes)) {
|
|
679
|
+
for (const shapeId of resolveLanePairs(links.affectedLanes)) {
|
|
680
|
+
if (!affected.includes(shapeId))
|
|
681
|
+
affected.push(shapeId);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
else if (Array.isArray(links.affectedRoads)) {
|
|
576
685
|
for (const rid of links.affectedRoads) {
|
|
577
686
|
if (typeof rid !== 'string')
|
|
578
687
|
continue;
|
|
@@ -615,10 +724,38 @@ export function odrToShapes(map, options = {}) {
|
|
|
615
724
|
for (const { road, samples } of signalRoads) {
|
|
616
725
|
materializeCrosswalks(road, samples);
|
|
617
726
|
}
|
|
618
|
-
// Restore right-of-way links stashed by the exporter
|
|
619
|
-
// <userData code="
|
|
620
|
-
//
|
|
727
|
+
// Restore right-of-way links stashed by the exporter.
|
|
728
|
+
// Current exports carry <userData code="yieldLanes"> with per-lane
|
|
729
|
+
// { ownLaneId: [[roadId, laneId], ...] } records; legacy files carried
|
|
730
|
+
// <userData code="yieldRoads"> (every driving lane of the carrying road
|
|
731
|
+
// yields over the driving lanes of the listed roads).
|
|
621
732
|
for (const road of roads) {
|
|
733
|
+
const rawYieldLanes = road.userData['yieldLanes'];
|
|
734
|
+
if (rawYieldLanes) {
|
|
735
|
+
let byLane;
|
|
736
|
+
try {
|
|
737
|
+
byLane = JSON.parse(rawYieldLanes);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
742
|
+
if (!byLane || typeof byLane !== 'object' || Array.isArray(byLane))
|
|
743
|
+
continue;
|
|
744
|
+
for (const [laneIdStr, pairs] of Object.entries(byLane)) {
|
|
745
|
+
const odrLaneId = parseInt(laneIdStr, 10);
|
|
746
|
+
if (!Number.isFinite(odrLaneId))
|
|
747
|
+
continue;
|
|
748
|
+
const yieldLaneIds = resolveLanePairs(pairs);
|
|
749
|
+
if (yieldLaneIds.length === 0)
|
|
750
|
+
continue;
|
|
751
|
+
for (const shapeId of lanesOfRoadLane(road.id, odrLaneId)) {
|
|
752
|
+
const lane = laneShapeById.get(shapeId);
|
|
753
|
+
if (lane)
|
|
754
|
+
lane.yieldLaneIds = [...yieldLaneIds];
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
622
759
|
const rawYield = road.userData['yieldRoads'];
|
|
623
760
|
if (!rawYield)
|
|
624
761
|
continue;
|
|
@@ -741,7 +878,7 @@ export function odrToShapes(map, options = {}) {
|
|
|
741
878
|
// Lanelet tags stashed by the exporter in <userData
|
|
742
879
|
// code="laneAttributes"> (speed_limit, turn_direction, location,
|
|
743
880
|
// one_way=no, exact subtype, custom tags) override the defaults.
|
|
744
|
-
...restoredLaneAttributes(road),
|
|
881
|
+
...restoredLaneAttributes(road, lane.id),
|
|
745
882
|
odr_type: lane.type,
|
|
746
883
|
odr_road_id: road.id,
|
|
747
884
|
odr_lane_id: String(lane.id),
|
|
@@ -819,6 +956,72 @@ export function odrToShapes(map, options = {}) {
|
|
|
819
956
|
}
|
|
820
957
|
return out;
|
|
821
958
|
};
|
|
959
|
+
/**
|
|
960
|
+
* Resolve a (road, contact, lane id) endpoint to materialized lanes.
|
|
961
|
+
*
|
|
962
|
+
* Within a road this is `lanesAt` (which already bridges skipped micro
|
|
963
|
+
* sections). When the whole road was skipped — e.g. the short synthesized
|
|
964
|
+
* junction connecting roads this exporter emits, or any sub-threshold road
|
|
965
|
+
* in third-party files — the resolution continues across the road: lane
|
|
966
|
+
* links are walked through its (skipped) sections to the far end, and the
|
|
967
|
+
* road-level link there is followed into the neighbouring road, recursively,
|
|
968
|
+
* so connectivity is bridged across skipped roads instead of being lost.
|
|
969
|
+
*/
|
|
970
|
+
const resolveLaneEndpoints = (roadId, contact, odrLaneId, depth = 0) => {
|
|
971
|
+
const direct = lanesAt(roadId, contact, odrLaneId);
|
|
972
|
+
if (direct.length > 0)
|
|
973
|
+
return direct.map(reg => ({ reg, odrLaneId, contact }));
|
|
974
|
+
const road = roadById.get(roadId);
|
|
975
|
+
if (!road || depth > 4)
|
|
976
|
+
return [];
|
|
977
|
+
// Only bridge across roads with no materialized lanes at all; partially
|
|
978
|
+
// materialized roads are fully handled by the in-road resolution above.
|
|
979
|
+
if ((lanesByRoad.get(roadId) ?? []).length > 0)
|
|
980
|
+
return [];
|
|
981
|
+
const n = road.laneSections.length;
|
|
982
|
+
if (n === 0)
|
|
983
|
+
return [];
|
|
984
|
+
// Walk lane-level links through the skipped sections to the far end.
|
|
985
|
+
const farContact = contact === 'start' ? 'end' : 'start';
|
|
986
|
+
const dir = contact === 'start' ? 1 : -1;
|
|
987
|
+
let idx = contact === 'start' ? 0 : n - 1;
|
|
988
|
+
let ids = [odrLaneId];
|
|
989
|
+
for (let step = 0; step < n - 1 && ids.length > 0; step++) {
|
|
990
|
+
const sec = road.laneSections[idx];
|
|
991
|
+
const nextIds = [];
|
|
992
|
+
for (const id of ids) {
|
|
993
|
+
const lane = [...sec.left, ...sec.right].find(l => l.id === id);
|
|
994
|
+
if (!lane)
|
|
995
|
+
continue;
|
|
996
|
+
for (const linked of dir === 1 ? lane.successorIds : lane.predecessorIds) {
|
|
997
|
+
if (!nextIds.includes(linked))
|
|
998
|
+
nextIds.push(linked);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
ids = nextIds;
|
|
1002
|
+
idx += dir;
|
|
1003
|
+
}
|
|
1004
|
+
const farSec = road.laneSections[farContact === 'start' ? 0 : n - 1];
|
|
1005
|
+
const link = farContact === 'end' ? road.successor : road.predecessor;
|
|
1006
|
+
if (!farSec || !link || link.elementType !== 'road')
|
|
1007
|
+
return [];
|
|
1008
|
+
const cpB = link.contactPoint ?? (farContact === 'end' ? 'start' : 'end');
|
|
1009
|
+
const out = [];
|
|
1010
|
+
for (const id of ids) {
|
|
1011
|
+
const lane = [...farSec.left, ...farSec.right].find(l => l.id === id);
|
|
1012
|
+
if (!lane)
|
|
1013
|
+
continue;
|
|
1014
|
+
const targetIds = farContact === 'end' ? lane.successorIds : lane.predecessorIds;
|
|
1015
|
+
for (const tid of targetIds) {
|
|
1016
|
+
for (const ep of resolveLaneEndpoints(link.elementId, cpB, tid, depth + 1)) {
|
|
1017
|
+
if (!out.some(o => o.reg === ep.reg && o.odrLaneId === ep.odrLaneId && o.contact === ep.contact)) {
|
|
1018
|
+
out.push(ep);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
return out;
|
|
1024
|
+
};
|
|
822
1025
|
/**
|
|
823
1026
|
* Link two lanes meeting at a shared contact, respecting travel direction:
|
|
824
1027
|
* right lanes (id < 0) travel toward the road's end, left lanes (id > 0)
|
|
@@ -887,10 +1090,10 @@ export function odrToShapes(map, options = {}) {
|
|
|
887
1090
|
return;
|
|
888
1091
|
for (const lane of [...sec.left, ...sec.right]) {
|
|
889
1092
|
const targetIds = cpA === 'end' ? lane.successorIds : lane.predecessorIds;
|
|
890
|
-
for (const a of
|
|
1093
|
+
for (const a of resolveLaneEndpoints(roadA.id, cpA, lane.id)) {
|
|
891
1094
|
for (const toId of targetIds) {
|
|
892
|
-
for (const b of
|
|
893
|
-
linkLanes(a,
|
|
1095
|
+
for (const b of resolveLaneEndpoints(roadB.id, cpB, toId)) {
|
|
1096
|
+
linkLanes(a.reg, a.odrLaneId, a.contact, b.reg, b.odrLaneId, b.contact);
|
|
894
1097
|
}
|
|
895
1098
|
}
|
|
896
1099
|
}
|
|
@@ -919,15 +1122,101 @@ export function odrToShapes(map, options = {}) {
|
|
|
919
1122
|
contacts.push('end'); // Tolerant default.
|
|
920
1123
|
for (const cpA of contacts) {
|
|
921
1124
|
for (const ll of conn.laneLinks) {
|
|
922
|
-
for (const a of
|
|
923
|
-
for (const b of
|
|
924
|
-
linkLanes(a,
|
|
1125
|
+
for (const a of resolveLaneEndpoints(roadA.id, cpA, ll.from)) {
|
|
1126
|
+
for (const b of resolveLaneEndpoints(roadC.id, conn.contactPoint, ll.to)) {
|
|
1127
|
+
linkLanes(a.reg, a.odrLaneId, a.contact, b.reg, b.odrLaneId, b.contact);
|
|
925
1128
|
}
|
|
926
1129
|
}
|
|
927
1130
|
}
|
|
928
1131
|
}
|
|
929
1132
|
}
|
|
930
1133
|
}
|
|
1134
|
+
// Hidden lane links: edges the exporter could not express as standard
|
|
1135
|
+
// <link>/<laneLink> records because a contact width is zero (the OpenDRIVE
|
|
1136
|
+
// zero-width / appearing-lane link rules), stashed per road as
|
|
1137
|
+
// <userData code="hiddenLaneLinks" value="[{fr,fl,tr,tl},...]"> with
|
|
1138
|
+
// from-road / from-lane / to-road / to-lane ids (from end -> to start in
|
|
1139
|
+
// travel direction). Restored here into next/prev like any other link.
|
|
1140
|
+
for (const road of roads) {
|
|
1141
|
+
const raw = road.userData['hiddenLaneLinks'];
|
|
1142
|
+
if (!raw)
|
|
1143
|
+
continue;
|
|
1144
|
+
let recs;
|
|
1145
|
+
try {
|
|
1146
|
+
recs = JSON.parse(raw);
|
|
1147
|
+
}
|
|
1148
|
+
catch {
|
|
1149
|
+
continue; // Malformed userData JSON is ignored (third-party files).
|
|
1150
|
+
}
|
|
1151
|
+
if (!Array.isArray(recs))
|
|
1152
|
+
continue;
|
|
1153
|
+
for (const rec of recs) {
|
|
1154
|
+
if (!rec || typeof rec !== 'object')
|
|
1155
|
+
continue;
|
|
1156
|
+
const { fr, fl, tr, tl } = rec;
|
|
1157
|
+
if (typeof fl !== 'number' || typeof tl !== 'number')
|
|
1158
|
+
continue;
|
|
1159
|
+
const fromRoad = fr === undefined ? road.id : String(fr);
|
|
1160
|
+
const toRoad = tr === undefined ? road.id : String(tr);
|
|
1161
|
+
for (const a of resolveLaneEndpoints(fromRoad, 'end', fl)) {
|
|
1162
|
+
for (const b of resolveLaneEndpoints(toRoad, 'start', tl)) {
|
|
1163
|
+
linkLanes(a.reg, a.odrLaneId, a.contact, b.reg, b.odrLaneId, b.contact);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// Junction <priority high low> records restore right-of-way links: the
|
|
1169
|
+
// lanes standing in for the prioritized connecting road gain yieldLaneIds
|
|
1170
|
+
// over the lanes standing in for the yielding one. A materialized
|
|
1171
|
+
// connecting road is represented by its own lanes; a skipped (short
|
|
1172
|
+
// synthesized) one resolves through its predecessor link to the incoming
|
|
1173
|
+
// lanes the maneuver started from — the lanes the exporter originally read
|
|
1174
|
+
// the yieldLaneIds off. Merged with any userData-restored links above.
|
|
1175
|
+
const priorityRoadLanes = (roadId) => {
|
|
1176
|
+
const own = lanesByRoad.get(roadId) ?? [];
|
|
1177
|
+
if (own.length > 0) {
|
|
1178
|
+
const out = [];
|
|
1179
|
+
for (const reg of own) {
|
|
1180
|
+
if (!out.includes(reg.shapeId))
|
|
1181
|
+
out.push(reg.shapeId);
|
|
1182
|
+
}
|
|
1183
|
+
return out;
|
|
1184
|
+
}
|
|
1185
|
+
const road = roadById.get(roadId);
|
|
1186
|
+
if (!road)
|
|
1187
|
+
return [];
|
|
1188
|
+
const lastSec = road.laneSections[road.laneSections.length - 1];
|
|
1189
|
+
if (!lastSec)
|
|
1190
|
+
return [];
|
|
1191
|
+
const out = [];
|
|
1192
|
+
for (const lane of [...lastSec.left, ...lastSec.right]) {
|
|
1193
|
+
for (const ep of resolveLaneEndpoints(roadId, 'end', lane.id)) {
|
|
1194
|
+
if (!out.includes(ep.reg.shapeId))
|
|
1195
|
+
out.push(ep.reg.shapeId);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
return out;
|
|
1199
|
+
};
|
|
1200
|
+
for (const junction of map.junctions) {
|
|
1201
|
+
for (const pr of junction.priorities) {
|
|
1202
|
+
const highLanes = priorityRoadLanes(pr.high);
|
|
1203
|
+
const lowLanes = priorityRoadLanes(pr.low);
|
|
1204
|
+
if (highLanes.length === 0 || lowLanes.length === 0)
|
|
1205
|
+
continue;
|
|
1206
|
+
for (const shapeId of highLanes) {
|
|
1207
|
+
const lane = laneShapeById.get(shapeId);
|
|
1208
|
+
if (!lane)
|
|
1209
|
+
continue;
|
|
1210
|
+
const merged = lane.yieldLaneIds ?? [];
|
|
1211
|
+
for (const lowId of lowLanes) {
|
|
1212
|
+
if (lowId !== shapeId && !merged.includes(lowId))
|
|
1213
|
+
merged.push(lowId);
|
|
1214
|
+
}
|
|
1215
|
+
if (merged.length > 0)
|
|
1216
|
+
lane.yieldLaneIds = merged;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
931
1220
|
// ---- Round-trip fidelity post-processing ----
|
|
932
1221
|
// 1. Boundaries that are geometrically one line (each exported road carries
|
|
933
1222
|
// its own copy of a boundary shared with its neighbour) collapse into a
|
|
@@ -938,6 +1227,107 @@ export function odrToShapes(map, options = {}) {
|
|
|
938
1227
|
dedupeSharedBoundaries(result);
|
|
939
1228
|
weldConnectedLaneContacts(result);
|
|
940
1229
|
removeOrphanPoints(result);
|
|
1230
|
+
// ---- Carry-through records ----
|
|
1231
|
+
// Per-road state hashes for `exportToOpenDrive({ sidecar })`: a road whose
|
|
1232
|
+
// hash still matches at export time was not edited and is re-emitted
|
|
1233
|
+
// verbatim from the sidecar XML. Hashes are taken AFTER all post-processing
|
|
1234
|
+
// (dedupe / weld / orphan removal) so they describe exactly the shapes the
|
|
1235
|
+
// editor will hold; the exporter recomputes them from the live shapes.
|
|
1236
|
+
{
|
|
1237
|
+
const recPointById = new Map(result.points.map(p => [p.id, p]));
|
|
1238
|
+
const recLsById = new Map(result.linestrings.map(l => [l.id, l]));
|
|
1239
|
+
const boundaryPts = (lsId, invert) => {
|
|
1240
|
+
if (!lsId)
|
|
1241
|
+
return null;
|
|
1242
|
+
const ls = recLsById.get(lsId);
|
|
1243
|
+
if (!ls)
|
|
1244
|
+
return null;
|
|
1245
|
+
const ids = invert ? [...ls.pointIds].reverse() : ls.pointIds;
|
|
1246
|
+
const pts = [];
|
|
1247
|
+
for (const pid of ids) {
|
|
1248
|
+
const p = recPointById.get(pid);
|
|
1249
|
+
if (p)
|
|
1250
|
+
pts.push({ x: p.x, y: p.y });
|
|
1251
|
+
}
|
|
1252
|
+
return pts.length >= 2 ? pts : null;
|
|
1253
|
+
};
|
|
1254
|
+
const laneRoadOf = new Map();
|
|
1255
|
+
for (const [roadId, regs] of lanesByRoad) {
|
|
1256
|
+
for (const reg of regs)
|
|
1257
|
+
laneRoadOf.set(reg.shapeId, roadId);
|
|
1258
|
+
}
|
|
1259
|
+
// Regulatory shapes touching a road: attached to it (odr_road_id) or
|
|
1260
|
+
// affecting any of its lanes. Mirrored by the exporter's hash builder.
|
|
1261
|
+
const regStatesByRoad = new Map();
|
|
1262
|
+
const addRegState = (state, affected, own) => {
|
|
1263
|
+
const touching = new Set();
|
|
1264
|
+
if (own && roadById.has(own))
|
|
1265
|
+
touching.add(own);
|
|
1266
|
+
for (const lid of affected) {
|
|
1267
|
+
const rid = laneRoadOf.get(lid);
|
|
1268
|
+
if (rid)
|
|
1269
|
+
touching.add(rid);
|
|
1270
|
+
}
|
|
1271
|
+
for (const rid of touching) {
|
|
1272
|
+
const list = regStatesByRoad.get(rid) ?? [];
|
|
1273
|
+
list.push(state);
|
|
1274
|
+
regStatesByRoad.set(rid, list);
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
for (const tl of result.trafficLights) {
|
|
1278
|
+
addRegState({
|
|
1279
|
+
kind: 'traffic_light',
|
|
1280
|
+
shapeId: tl.id,
|
|
1281
|
+
numbers: [tl.x, tl.y, tl.w, tl.h, 0],
|
|
1282
|
+
attributes: tl.attributes,
|
|
1283
|
+
affectedLaneIds: tl.affectedLaneIds,
|
|
1284
|
+
stopLinePts: boundaryPts(tl.stopLineId, false),
|
|
1285
|
+
controllerId: '',
|
|
1286
|
+
}, tl.affectedLaneIds, tl.attributes['odr_road_id']);
|
|
1287
|
+
}
|
|
1288
|
+
for (const ts of result.trafficSigns) {
|
|
1289
|
+
addRegState({
|
|
1290
|
+
kind: 'traffic_sign',
|
|
1291
|
+
shapeId: ts.id,
|
|
1292
|
+
numbers: [ts.x, ts.y, ts.w, ts.h, 0],
|
|
1293
|
+
attributes: ts.attributes,
|
|
1294
|
+
affectedLaneIds: ts.affectedLaneIds,
|
|
1295
|
+
stopLinePts: boundaryPts(ts.stopLineId, false),
|
|
1296
|
+
controllerId: '',
|
|
1297
|
+
}, ts.affectedLaneIds, ts.attributes['odr_road_id']);
|
|
1298
|
+
}
|
|
1299
|
+
for (const cw of result.crosswalks) {
|
|
1300
|
+
addRegState({
|
|
1301
|
+
kind: 'crosswalk',
|
|
1302
|
+
shapeId: cw.id,
|
|
1303
|
+
numbers: [cw.x, cw.y, cw.startX, cw.startY, cw.endX, cw.endY, cw.crosswalkWidth, 0],
|
|
1304
|
+
attributes: cw.attributes,
|
|
1305
|
+
affectedLaneIds: cw.affectedLaneIds,
|
|
1306
|
+
stopLinePts: boundaryPts(cw.stopLineId, false),
|
|
1307
|
+
controllerId: '',
|
|
1308
|
+
}, cw.affectedLaneIds, cw.attributes['odr_road_id']);
|
|
1309
|
+
}
|
|
1310
|
+
const roadRecords = {};
|
|
1311
|
+
for (const road of roads) {
|
|
1312
|
+
const regLanes = lanesByRoad.get(road.id) ?? [];
|
|
1313
|
+
const laneStates = regLanes.map(reg => {
|
|
1314
|
+
const lane = laneShapeById.get(reg.shapeId);
|
|
1315
|
+
return {
|
|
1316
|
+
leftPts: boundaryPts(lane.leftBoundaryId, lane.invertLeft),
|
|
1317
|
+
rightPts: boundaryPts(lane.rightBoundaryId, lane.invertRight),
|
|
1318
|
+
attributes: lane.attributes,
|
|
1319
|
+
next: lane.next,
|
|
1320
|
+
prev: lane.prev,
|
|
1321
|
+
yieldLaneIds: lane.yieldLaneIds ?? [],
|
|
1322
|
+
};
|
|
1323
|
+
});
|
|
1324
|
+
roadRecords[road.id] = {
|
|
1325
|
+
laneShapeIds: regLanes.map(r => r.shapeId),
|
|
1326
|
+
stateHash: hashRoadState(laneStates, regStatesByRoad.get(road.id) ?? []),
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
result.sidecar.roadRecords = roadRecords;
|
|
1330
|
+
}
|
|
941
1331
|
// ---- Aggregated warnings ----
|
|
942
1332
|
if (elevationRoads > 0) {
|
|
943
1333
|
warnings.push(`Elevation profiles on ${elevationRoads} road(s) were flattened to 2D.`);
|