@drawtonomy/sdk 0.9.0 → 0.10.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 +2 -2
- package/dist/exporter/index.d.ts.map +1 -1
- package/dist/exporter/index.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 +361 -35
- 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 +1521 -296
- package/dist/exporter/opendrive.js.map +1 -1
- package/dist/exporter/opendriveParser.d.ts +8 -0
- package/dist/exporter/opendriveParser.d.ts.map +1 -1
- package/dist/exporter/opendriveParser.js +5 -2
- package/dist/exporter/opendriveParser.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/package.json +1 -1
|
@@ -29,6 +29,7 @@ import { evalPoly3, sampleReferenceLine } from './odrGeometry';
|
|
|
29
29
|
import { sampleAtParam } from './laneCenterline';
|
|
30
30
|
import { createShapeIdAllocator, } from './osmToShapes';
|
|
31
31
|
import { PIXELS_PER_METER } from './units';
|
|
32
|
+
import { hashRoadState, } from './odrCarryThrough';
|
|
32
33
|
/** Lanes narrower than this (m) carry no usable area and are skipped. */
|
|
33
34
|
const WIDTH_EPS = 1e-3;
|
|
34
35
|
const S_EPS = 1e-6;
|
|
@@ -255,35 +256,52 @@ export function odrToShapes(map, options = {}) {
|
|
|
255
256
|
const roadById = new Map();
|
|
256
257
|
/** Roads (with their reference samples) whose signals are converted after all lanes exist. */
|
|
257
258
|
const signalRoads = [];
|
|
258
|
-
/** Lane attributes restored from <userData code="laneAttributes"> per road. */
|
|
259
259
|
const restoredAttrCache = new Map();
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
260
|
+
const pickStringAttrs = (obj) => {
|
|
261
|
+
const out = {};
|
|
262
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
263
|
+
// `type` is fixed to 'lanelet' and odr_* meta is regenerated.
|
|
264
|
+
if (typeof v !== 'string' || k === 'type' || k.startsWith('odr_'))
|
|
265
|
+
continue;
|
|
266
|
+
out[k] = v;
|
|
267
|
+
}
|
|
268
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
269
|
+
};
|
|
270
|
+
const restoredLaneAttributes = (road, odrLaneId) => {
|
|
271
|
+
let entry = restoredAttrCache.get(road.id);
|
|
272
|
+
if (entry === undefined) {
|
|
273
|
+
entry = { flat: null, perLane: null };
|
|
274
|
+
const raw = road.userData['laneAttributes'];
|
|
275
|
+
if (raw) {
|
|
276
|
+
try {
|
|
277
|
+
const obj = JSON.parse(raw);
|
|
278
|
+
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
|
|
279
|
+
const flat = {};
|
|
280
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
281
|
+
if (v && typeof v === 'object' && !Array.isArray(v) && /^-?\d+$/.test(k)) {
|
|
282
|
+
const laneAttrs = pickStringAttrs(v);
|
|
283
|
+
if (laneAttrs) {
|
|
284
|
+
entry.perLane = entry.perLane ?? new Map();
|
|
285
|
+
entry.perLane.set(parseInt(k, 10), laneAttrs);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
flat[k] = v;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
entry.flat = pickStringAttrs(flat);
|
|
276
293
|
}
|
|
277
|
-
|
|
278
|
-
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Malformed userData JSON is ignored (third-party files).
|
|
279
297
|
}
|
|
280
298
|
}
|
|
281
|
-
|
|
282
|
-
// Malformed userData JSON is ignored (third-party files).
|
|
283
|
-
}
|
|
299
|
+
restoredAttrCache.set(road.id, entry);
|
|
284
300
|
}
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
const perLane = entry.perLane?.get(odrLaneId) ?? null;
|
|
302
|
+
if (!entry.flat && !perLane)
|
|
303
|
+
return null;
|
|
304
|
+
return { ...(entry.flat ?? {}), ...(perLane ?? {}) };
|
|
287
305
|
};
|
|
288
306
|
const registryKey = (roadId, sectionIdx, odrLaneId) => `${roadId}|${sectionIdx}|${odrLaneId}`;
|
|
289
307
|
for (const road of roads) {
|
|
@@ -539,6 +557,36 @@ export function odrToShapes(map, options = {}) {
|
|
|
539
557
|
}
|
|
540
558
|
return out;
|
|
541
559
|
};
|
|
560
|
+
/** Shape ids of a specific (road, ODR lane id) pair, across all sections. */
|
|
561
|
+
const lanesOfRoadLane = (roadId, odrLaneId) => {
|
|
562
|
+
const out = [];
|
|
563
|
+
for (const reg of lanesByRoad.get(roadId) ?? []) {
|
|
564
|
+
if (reg.odrLaneId === odrLaneId && !out.includes(reg.shapeId))
|
|
565
|
+
out.push(reg.shapeId);
|
|
566
|
+
}
|
|
567
|
+
return out;
|
|
568
|
+
};
|
|
569
|
+
/** Resolve a [[roadId, laneId], ...] JSON record to lane shape ids. */
|
|
570
|
+
const resolveLanePairs = (pairs) => {
|
|
571
|
+
const out = [];
|
|
572
|
+
if (!Array.isArray(pairs))
|
|
573
|
+
return out;
|
|
574
|
+
for (const entry of pairs) {
|
|
575
|
+
if (!Array.isArray(entry) || entry.length < 2)
|
|
576
|
+
continue;
|
|
577
|
+
const [rid, lid] = entry;
|
|
578
|
+
if (typeof rid !== 'string' || typeof lid !== 'string')
|
|
579
|
+
continue;
|
|
580
|
+
const odrLaneId = parseInt(lid, 10);
|
|
581
|
+
if (!Number.isFinite(odrLaneId))
|
|
582
|
+
continue;
|
|
583
|
+
for (const shapeId of lanesOfRoadLane(rid, odrLaneId)) {
|
|
584
|
+
if (!out.includes(shapeId))
|
|
585
|
+
out.push(shapeId);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return out;
|
|
589
|
+
};
|
|
542
590
|
/**
|
|
543
591
|
* Convert crosswalk objects into crosswalk shapes. The band center is the
|
|
544
592
|
* (s, t) station on the reference line; the walking axis follows the
|
|
@@ -572,7 +620,15 @@ export function odrToShapes(map, options = {}) {
|
|
|
572
620
|
if (rawLinks) {
|
|
573
621
|
try {
|
|
574
622
|
const links = JSON.parse(rawLinks);
|
|
575
|
-
|
|
623
|
+
// Current exports carry lane-precise [[roadId, laneId], ...] pairs;
|
|
624
|
+
// legacy files only listed road ids (=> every driving lane).
|
|
625
|
+
if (Array.isArray(links.affectedLanes)) {
|
|
626
|
+
for (const shapeId of resolveLanePairs(links.affectedLanes)) {
|
|
627
|
+
if (!affected.includes(shapeId))
|
|
628
|
+
affected.push(shapeId);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
else if (Array.isArray(links.affectedRoads)) {
|
|
576
632
|
for (const rid of links.affectedRoads) {
|
|
577
633
|
if (typeof rid !== 'string')
|
|
578
634
|
continue;
|
|
@@ -615,10 +671,38 @@ export function odrToShapes(map, options = {}) {
|
|
|
615
671
|
for (const { road, samples } of signalRoads) {
|
|
616
672
|
materializeCrosswalks(road, samples);
|
|
617
673
|
}
|
|
618
|
-
// Restore right-of-way links stashed by the exporter
|
|
619
|
-
// <userData code="
|
|
620
|
-
//
|
|
674
|
+
// Restore right-of-way links stashed by the exporter.
|
|
675
|
+
// Current exports carry <userData code="yieldLanes"> with per-lane
|
|
676
|
+
// { ownLaneId: [[roadId, laneId], ...] } records; legacy files carried
|
|
677
|
+
// <userData code="yieldRoads"> (every driving lane of the carrying road
|
|
678
|
+
// yields over the driving lanes of the listed roads).
|
|
621
679
|
for (const road of roads) {
|
|
680
|
+
const rawYieldLanes = road.userData['yieldLanes'];
|
|
681
|
+
if (rawYieldLanes) {
|
|
682
|
+
let byLane;
|
|
683
|
+
try {
|
|
684
|
+
byLane = JSON.parse(rawYieldLanes);
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!byLane || typeof byLane !== 'object' || Array.isArray(byLane))
|
|
690
|
+
continue;
|
|
691
|
+
for (const [laneIdStr, pairs] of Object.entries(byLane)) {
|
|
692
|
+
const odrLaneId = parseInt(laneIdStr, 10);
|
|
693
|
+
if (!Number.isFinite(odrLaneId))
|
|
694
|
+
continue;
|
|
695
|
+
const yieldLaneIds = resolveLanePairs(pairs);
|
|
696
|
+
if (yieldLaneIds.length === 0)
|
|
697
|
+
continue;
|
|
698
|
+
for (const shapeId of lanesOfRoadLane(road.id, odrLaneId)) {
|
|
699
|
+
const lane = laneShapeById.get(shapeId);
|
|
700
|
+
if (lane)
|
|
701
|
+
lane.yieldLaneIds = [...yieldLaneIds];
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
continue;
|
|
705
|
+
}
|
|
622
706
|
const rawYield = road.userData['yieldRoads'];
|
|
623
707
|
if (!rawYield)
|
|
624
708
|
continue;
|
|
@@ -741,7 +825,7 @@ export function odrToShapes(map, options = {}) {
|
|
|
741
825
|
// Lanelet tags stashed by the exporter in <userData
|
|
742
826
|
// code="laneAttributes"> (speed_limit, turn_direction, location,
|
|
743
827
|
// one_way=no, exact subtype, custom tags) override the defaults.
|
|
744
|
-
...restoredLaneAttributes(road),
|
|
828
|
+
...restoredLaneAttributes(road, lane.id),
|
|
745
829
|
odr_type: lane.type,
|
|
746
830
|
odr_road_id: road.id,
|
|
747
831
|
odr_lane_id: String(lane.id),
|
|
@@ -819,6 +903,72 @@ export function odrToShapes(map, options = {}) {
|
|
|
819
903
|
}
|
|
820
904
|
return out;
|
|
821
905
|
};
|
|
906
|
+
/**
|
|
907
|
+
* Resolve a (road, contact, lane id) endpoint to materialized lanes.
|
|
908
|
+
*
|
|
909
|
+
* Within a road this is `lanesAt` (which already bridges skipped micro
|
|
910
|
+
* sections). When the whole road was skipped — e.g. the short synthesized
|
|
911
|
+
* junction connecting roads this exporter emits, or any sub-threshold road
|
|
912
|
+
* in third-party files — the resolution continues across the road: lane
|
|
913
|
+
* links are walked through its (skipped) sections to the far end, and the
|
|
914
|
+
* road-level link there is followed into the neighbouring road, recursively,
|
|
915
|
+
* so connectivity is bridged across skipped roads instead of being lost.
|
|
916
|
+
*/
|
|
917
|
+
const resolveLaneEndpoints = (roadId, contact, odrLaneId, depth = 0) => {
|
|
918
|
+
const direct = lanesAt(roadId, contact, odrLaneId);
|
|
919
|
+
if (direct.length > 0)
|
|
920
|
+
return direct.map(reg => ({ reg, odrLaneId, contact }));
|
|
921
|
+
const road = roadById.get(roadId);
|
|
922
|
+
if (!road || depth > 4)
|
|
923
|
+
return [];
|
|
924
|
+
// Only bridge across roads with no materialized lanes at all; partially
|
|
925
|
+
// materialized roads are fully handled by the in-road resolution above.
|
|
926
|
+
if ((lanesByRoad.get(roadId) ?? []).length > 0)
|
|
927
|
+
return [];
|
|
928
|
+
const n = road.laneSections.length;
|
|
929
|
+
if (n === 0)
|
|
930
|
+
return [];
|
|
931
|
+
// Walk lane-level links through the skipped sections to the far end.
|
|
932
|
+
const farContact = contact === 'start' ? 'end' : 'start';
|
|
933
|
+
const dir = contact === 'start' ? 1 : -1;
|
|
934
|
+
let idx = contact === 'start' ? 0 : n - 1;
|
|
935
|
+
let ids = [odrLaneId];
|
|
936
|
+
for (let step = 0; step < n - 1 && ids.length > 0; step++) {
|
|
937
|
+
const sec = road.laneSections[idx];
|
|
938
|
+
const nextIds = [];
|
|
939
|
+
for (const id of ids) {
|
|
940
|
+
const lane = [...sec.left, ...sec.right].find(l => l.id === id);
|
|
941
|
+
if (!lane)
|
|
942
|
+
continue;
|
|
943
|
+
for (const linked of dir === 1 ? lane.successorIds : lane.predecessorIds) {
|
|
944
|
+
if (!nextIds.includes(linked))
|
|
945
|
+
nextIds.push(linked);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
ids = nextIds;
|
|
949
|
+
idx += dir;
|
|
950
|
+
}
|
|
951
|
+
const farSec = road.laneSections[farContact === 'start' ? 0 : n - 1];
|
|
952
|
+
const link = farContact === 'end' ? road.successor : road.predecessor;
|
|
953
|
+
if (!farSec || !link || link.elementType !== 'road')
|
|
954
|
+
return [];
|
|
955
|
+
const cpB = link.contactPoint ?? (farContact === 'end' ? 'start' : 'end');
|
|
956
|
+
const out = [];
|
|
957
|
+
for (const id of ids) {
|
|
958
|
+
const lane = [...farSec.left, ...farSec.right].find(l => l.id === id);
|
|
959
|
+
if (!lane)
|
|
960
|
+
continue;
|
|
961
|
+
const targetIds = farContact === 'end' ? lane.successorIds : lane.predecessorIds;
|
|
962
|
+
for (const tid of targetIds) {
|
|
963
|
+
for (const ep of resolveLaneEndpoints(link.elementId, cpB, tid, depth + 1)) {
|
|
964
|
+
if (!out.some(o => o.reg === ep.reg && o.odrLaneId === ep.odrLaneId && o.contact === ep.contact)) {
|
|
965
|
+
out.push(ep);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
return out;
|
|
971
|
+
};
|
|
822
972
|
/**
|
|
823
973
|
* Link two lanes meeting at a shared contact, respecting travel direction:
|
|
824
974
|
* right lanes (id < 0) travel toward the road's end, left lanes (id > 0)
|
|
@@ -887,10 +1037,10 @@ export function odrToShapes(map, options = {}) {
|
|
|
887
1037
|
return;
|
|
888
1038
|
for (const lane of [...sec.left, ...sec.right]) {
|
|
889
1039
|
const targetIds = cpA === 'end' ? lane.successorIds : lane.predecessorIds;
|
|
890
|
-
for (const a of
|
|
1040
|
+
for (const a of resolveLaneEndpoints(roadA.id, cpA, lane.id)) {
|
|
891
1041
|
for (const toId of targetIds) {
|
|
892
|
-
for (const b of
|
|
893
|
-
linkLanes(a,
|
|
1042
|
+
for (const b of resolveLaneEndpoints(roadB.id, cpB, toId)) {
|
|
1043
|
+
linkLanes(a.reg, a.odrLaneId, a.contact, b.reg, b.odrLaneId, b.contact);
|
|
894
1044
|
}
|
|
895
1045
|
}
|
|
896
1046
|
}
|
|
@@ -919,15 +1069,101 @@ export function odrToShapes(map, options = {}) {
|
|
|
919
1069
|
contacts.push('end'); // Tolerant default.
|
|
920
1070
|
for (const cpA of contacts) {
|
|
921
1071
|
for (const ll of conn.laneLinks) {
|
|
922
|
-
for (const a of
|
|
923
|
-
for (const b of
|
|
924
|
-
linkLanes(a,
|
|
1072
|
+
for (const a of resolveLaneEndpoints(roadA.id, cpA, ll.from)) {
|
|
1073
|
+
for (const b of resolveLaneEndpoints(roadC.id, conn.contactPoint, ll.to)) {
|
|
1074
|
+
linkLanes(a.reg, a.odrLaneId, a.contact, b.reg, b.odrLaneId, b.contact);
|
|
925
1075
|
}
|
|
926
1076
|
}
|
|
927
1077
|
}
|
|
928
1078
|
}
|
|
929
1079
|
}
|
|
930
1080
|
}
|
|
1081
|
+
// Hidden lane links: edges the exporter could not express as standard
|
|
1082
|
+
// <link>/<laneLink> records because a contact width is zero (the OpenDRIVE
|
|
1083
|
+
// zero-width / appearing-lane link rules), stashed per road as
|
|
1084
|
+
// <userData code="hiddenLaneLinks" value="[{fr,fl,tr,tl},...]"> with
|
|
1085
|
+
// from-road / from-lane / to-road / to-lane ids (from end -> to start in
|
|
1086
|
+
// travel direction). Restored here into next/prev like any other link.
|
|
1087
|
+
for (const road of roads) {
|
|
1088
|
+
const raw = road.userData['hiddenLaneLinks'];
|
|
1089
|
+
if (!raw)
|
|
1090
|
+
continue;
|
|
1091
|
+
let recs;
|
|
1092
|
+
try {
|
|
1093
|
+
recs = JSON.parse(raw);
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
continue; // Malformed userData JSON is ignored (third-party files).
|
|
1097
|
+
}
|
|
1098
|
+
if (!Array.isArray(recs))
|
|
1099
|
+
continue;
|
|
1100
|
+
for (const rec of recs) {
|
|
1101
|
+
if (!rec || typeof rec !== 'object')
|
|
1102
|
+
continue;
|
|
1103
|
+
const { fr, fl, tr, tl } = rec;
|
|
1104
|
+
if (typeof fl !== 'number' || typeof tl !== 'number')
|
|
1105
|
+
continue;
|
|
1106
|
+
const fromRoad = fr === undefined ? road.id : String(fr);
|
|
1107
|
+
const toRoad = tr === undefined ? road.id : String(tr);
|
|
1108
|
+
for (const a of resolveLaneEndpoints(fromRoad, 'end', fl)) {
|
|
1109
|
+
for (const b of resolveLaneEndpoints(toRoad, 'start', tl)) {
|
|
1110
|
+
linkLanes(a.reg, a.odrLaneId, a.contact, b.reg, b.odrLaneId, b.contact);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
// Junction <priority high low> records restore right-of-way links: the
|
|
1116
|
+
// lanes standing in for the prioritized connecting road gain yieldLaneIds
|
|
1117
|
+
// over the lanes standing in for the yielding one. A materialized
|
|
1118
|
+
// connecting road is represented by its own lanes; a skipped (short
|
|
1119
|
+
// synthesized) one resolves through its predecessor link to the incoming
|
|
1120
|
+
// lanes the maneuver started from — the lanes the exporter originally read
|
|
1121
|
+
// the yieldLaneIds off. Merged with any userData-restored links above.
|
|
1122
|
+
const priorityRoadLanes = (roadId) => {
|
|
1123
|
+
const own = lanesByRoad.get(roadId) ?? [];
|
|
1124
|
+
if (own.length > 0) {
|
|
1125
|
+
const out = [];
|
|
1126
|
+
for (const reg of own) {
|
|
1127
|
+
if (!out.includes(reg.shapeId))
|
|
1128
|
+
out.push(reg.shapeId);
|
|
1129
|
+
}
|
|
1130
|
+
return out;
|
|
1131
|
+
}
|
|
1132
|
+
const road = roadById.get(roadId);
|
|
1133
|
+
if (!road)
|
|
1134
|
+
return [];
|
|
1135
|
+
const lastSec = road.laneSections[road.laneSections.length - 1];
|
|
1136
|
+
if (!lastSec)
|
|
1137
|
+
return [];
|
|
1138
|
+
const out = [];
|
|
1139
|
+
for (const lane of [...lastSec.left, ...lastSec.right]) {
|
|
1140
|
+
for (const ep of resolveLaneEndpoints(roadId, 'end', lane.id)) {
|
|
1141
|
+
if (!out.includes(ep.reg.shapeId))
|
|
1142
|
+
out.push(ep.reg.shapeId);
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
return out;
|
|
1146
|
+
};
|
|
1147
|
+
for (const junction of map.junctions) {
|
|
1148
|
+
for (const pr of junction.priorities) {
|
|
1149
|
+
const highLanes = priorityRoadLanes(pr.high);
|
|
1150
|
+
const lowLanes = priorityRoadLanes(pr.low);
|
|
1151
|
+
if (highLanes.length === 0 || lowLanes.length === 0)
|
|
1152
|
+
continue;
|
|
1153
|
+
for (const shapeId of highLanes) {
|
|
1154
|
+
const lane = laneShapeById.get(shapeId);
|
|
1155
|
+
if (!lane)
|
|
1156
|
+
continue;
|
|
1157
|
+
const merged = lane.yieldLaneIds ?? [];
|
|
1158
|
+
for (const lowId of lowLanes) {
|
|
1159
|
+
if (lowId !== shapeId && !merged.includes(lowId))
|
|
1160
|
+
merged.push(lowId);
|
|
1161
|
+
}
|
|
1162
|
+
if (merged.length > 0)
|
|
1163
|
+
lane.yieldLaneIds = merged;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
931
1167
|
// ---- Round-trip fidelity post-processing ----
|
|
932
1168
|
// 1. Boundaries that are geometrically one line (each exported road carries
|
|
933
1169
|
// its own copy of a boundary shared with its neighbour) collapse into a
|
|
@@ -938,6 +1174,96 @@ export function odrToShapes(map, options = {}) {
|
|
|
938
1174
|
dedupeSharedBoundaries(result);
|
|
939
1175
|
weldConnectedLaneContacts(result);
|
|
940
1176
|
removeOrphanPoints(result);
|
|
1177
|
+
// ---- Carry-through records ----
|
|
1178
|
+
// Per-road state hashes for `exportToOpenDrive({ sidecar })`: a road whose
|
|
1179
|
+
// hash still matches at export time was not edited and is re-emitted
|
|
1180
|
+
// verbatim from the sidecar XML. Hashes are taken AFTER all post-processing
|
|
1181
|
+
// (dedupe / weld / orphan removal) so they describe exactly the shapes the
|
|
1182
|
+
// editor will hold; the exporter recomputes them from the live shapes.
|
|
1183
|
+
{
|
|
1184
|
+
const recPointById = new Map(result.points.map(p => [p.id, p]));
|
|
1185
|
+
const recLsById = new Map(result.linestrings.map(l => [l.id, l]));
|
|
1186
|
+
const boundaryPts = (lsId, invert) => {
|
|
1187
|
+
if (!lsId)
|
|
1188
|
+
return null;
|
|
1189
|
+
const ls = recLsById.get(lsId);
|
|
1190
|
+
if (!ls)
|
|
1191
|
+
return null;
|
|
1192
|
+
const ids = invert ? [...ls.pointIds].reverse() : ls.pointIds;
|
|
1193
|
+
const pts = [];
|
|
1194
|
+
for (const pid of ids) {
|
|
1195
|
+
const p = recPointById.get(pid);
|
|
1196
|
+
if (p)
|
|
1197
|
+
pts.push({ x: p.x, y: p.y });
|
|
1198
|
+
}
|
|
1199
|
+
return pts.length >= 2 ? pts : null;
|
|
1200
|
+
};
|
|
1201
|
+
const laneRoadOf = new Map();
|
|
1202
|
+
for (const [roadId, regs] of lanesByRoad) {
|
|
1203
|
+
for (const reg of regs)
|
|
1204
|
+
laneRoadOf.set(reg.shapeId, roadId);
|
|
1205
|
+
}
|
|
1206
|
+
// Regulatory shapes touching a road: attached to it (odr_road_id) or
|
|
1207
|
+
// affecting any of its lanes. Mirrored by the exporter's hash builder.
|
|
1208
|
+
const regStatesByRoad = new Map();
|
|
1209
|
+
const addRegState = (state, affected, own) => {
|
|
1210
|
+
const touching = new Set();
|
|
1211
|
+
if (own && roadById.has(own))
|
|
1212
|
+
touching.add(own);
|
|
1213
|
+
for (const lid of affected) {
|
|
1214
|
+
const rid = laneRoadOf.get(lid);
|
|
1215
|
+
if (rid)
|
|
1216
|
+
touching.add(rid);
|
|
1217
|
+
}
|
|
1218
|
+
for (const rid of touching) {
|
|
1219
|
+
const list = regStatesByRoad.get(rid) ?? [];
|
|
1220
|
+
list.push(state);
|
|
1221
|
+
regStatesByRoad.set(rid, list);
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
for (const tl of result.trafficLights) {
|
|
1225
|
+
addRegState({
|
|
1226
|
+
kind: 'traffic_light',
|
|
1227
|
+
shapeId: tl.id,
|
|
1228
|
+
numbers: [tl.x, tl.y, tl.w, tl.h, 0],
|
|
1229
|
+
attributes: tl.attributes,
|
|
1230
|
+
affectedLaneIds: tl.affectedLaneIds,
|
|
1231
|
+
stopLinePts: boundaryPts(tl.stopLineId, false),
|
|
1232
|
+
controllerId: '',
|
|
1233
|
+
}, tl.affectedLaneIds, tl.attributes['odr_road_id']);
|
|
1234
|
+
}
|
|
1235
|
+
for (const cw of result.crosswalks) {
|
|
1236
|
+
addRegState({
|
|
1237
|
+
kind: 'crosswalk',
|
|
1238
|
+
shapeId: cw.id,
|
|
1239
|
+
numbers: [cw.x, cw.y, cw.startX, cw.startY, cw.endX, cw.endY, cw.crosswalkWidth, 0],
|
|
1240
|
+
attributes: cw.attributes,
|
|
1241
|
+
affectedLaneIds: cw.affectedLaneIds,
|
|
1242
|
+
stopLinePts: boundaryPts(cw.stopLineId, false),
|
|
1243
|
+
controllerId: '',
|
|
1244
|
+
}, cw.affectedLaneIds, cw.attributes['odr_road_id']);
|
|
1245
|
+
}
|
|
1246
|
+
const roadRecords = {};
|
|
1247
|
+
for (const road of roads) {
|
|
1248
|
+
const regLanes = lanesByRoad.get(road.id) ?? [];
|
|
1249
|
+
const laneStates = regLanes.map(reg => {
|
|
1250
|
+
const lane = laneShapeById.get(reg.shapeId);
|
|
1251
|
+
return {
|
|
1252
|
+
leftPts: boundaryPts(lane.leftBoundaryId, lane.invertLeft),
|
|
1253
|
+
rightPts: boundaryPts(lane.rightBoundaryId, lane.invertRight),
|
|
1254
|
+
attributes: lane.attributes,
|
|
1255
|
+
next: lane.next,
|
|
1256
|
+
prev: lane.prev,
|
|
1257
|
+
yieldLaneIds: lane.yieldLaneIds ?? [],
|
|
1258
|
+
};
|
|
1259
|
+
});
|
|
1260
|
+
roadRecords[road.id] = {
|
|
1261
|
+
laneShapeIds: regLanes.map(r => r.shapeId),
|
|
1262
|
+
stateHash: hashRoadState(laneStates, regStatesByRoad.get(road.id) ?? []),
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
result.sidecar.roadRecords = roadRecords;
|
|
1266
|
+
}
|
|
941
1267
|
// ---- Aggregated warnings ----
|
|
942
1268
|
if (elevationRoads > 0) {
|
|
943
1269
|
warnings.push(`Elevation profiles on ${elevationRoads} road(s) were flattened to 2D.`);
|