11import { normaliseSeverity } from "@openconditions/core" ;
2- import type { Confidence } from "@openconditions/core" ;
2+ import type { Confidence , RecurringWindow } from "@openconditions/core" ;
33import type { Geometry } from "geojson" ;
44import type { Restriction , RoadEvent , UnresolvedRoadEvent } from "./model.js" ;
55import { dedupeRoadEvents } from "./dedupe.js" ;
@@ -115,7 +115,14 @@ function parseLatLonList(raw: string | undefined): [number, number][] {
115115 * no coordinate geometry (Alert-C/TMC only) return null (decoded in Phase 2).
116116 */
117117function resolveGeometry ( rec : XmlObject ) : Geometry | null {
118- const locRef = getXmlChild ( rec , "locationReference" ) ?? getXmlChild ( rec , "groupOfLocations" ) ;
118+ return resolveGeometryFrom (
119+ getXmlChild ( rec , "locationReference" ) ?? getXmlChild ( rec , "groupOfLocations" )
120+ ) ;
121+ }
122+
123+ /** Walk a location subtree (locationReference, groupOfLocations, alternativeRoute,
124+ * …) for any coordinate-bearing element and assemble a GeoJSON geometry. */
125+ function resolveGeometryFrom ( locRef : XmlObject | undefined ) : Geometry | null {
119126 if ( ! locRef ) return null ;
120127
121128 const lines : [ number , number ] [ ] [ ] = [ ] ;
@@ -164,6 +171,38 @@ function resolveGeometry(rec: XmlObject): Geometry | null {
164171 return null ;
165172}
166173
174+ /** The diversion route geometry from an `alternativeRoute`, when it is linear. */
175+ function detourGeometryOf ( rec : XmlObject ) : RoadEvent [ "detourGeometry" ] {
176+ const g = resolveGeometryFrom ( getXmlChild ( rec , "alternativeRoute" ) ) ;
177+ return g && ( g . type === "LineString" || g . type === "MultiLineString" ) ? g : undefined ;
178+ }
179+
180+ /**
181+ * validPeriod windows → schedule: each bounded date range (startOfPeriod /
182+ * endOfPeriod) plus the daily time window (recurringTimePeriodOfDay) when the
183+ * source supplies one. The overall start/end stay on validFrom/validTo.
184+ */
185+ function scheduleOf ( timeSpec : XmlObject | undefined ) : RecurringWindow [ ] | undefined {
186+ if ( ! timeSpec ) return undefined ;
187+ const out : RecurringWindow [ ] = [ ] ;
188+ for ( const vp of getXmlChildren ( timeSpec , "validPeriod" ) ) {
189+ const win : RecurringWindow = { } ;
190+ const ds = getXmlChildText ( vp , "startOfPeriod" ) ;
191+ const de = getXmlChildText ( vp , "endOfPeriod" ) ;
192+ if ( ds ) win . dateStart = ds ;
193+ if ( de ) win . dateEnd = de ;
194+ const tod = getXmlChild ( vp , "recurringTimePeriodOfDay" ) ;
195+ if ( tod ) {
196+ const ts = getXmlChildText ( tod , "startTimeOfPeriod" ) ;
197+ const te = getXmlChildText ( tod , "endTimeOfPeriod" ) ;
198+ if ( ts ) win . timeStart = ts ;
199+ if ( te ) win . timeEnd = te ;
200+ }
201+ if ( Object . keys ( win ) . length > 0 ) out . push ( win ) ;
202+ }
203+ return out . length > 0 ? out : undefined ;
204+ }
205+
167206function directionOf ( rec : XmlObject ) : string | undefined {
168207 const locRef = getXmlChild ( rec , "locationReference" ) ;
169208 if ( ! locRef ) return undefined ;
@@ -234,14 +273,33 @@ function lanesOf(rec: XmlObject): RoadEvent["lanesAffected"] | undefined {
234273 return lanes . closed != null || lanes . total != null ? lanes : undefined ;
235274}
236275
237- /** Source cause/obstruction subtype (e.g. "roadMaintenance", "brokenDownVehicle"). */
238- function causeOf ( rec : XmlObject ) : string | undefined {
276+ /**
277+ * The most specific source sub-classification for the record. The cause's
278+ * causeType wins when present (e.g. "roadMaintenance"); otherwise the record's
279+ * own typed discriminator is used — each DATEX situationRecord subclass carries
280+ * its own (accidentType, obstructionType, generalNetworkManagementType for
281+ * movable-bridge openings, abnormalTrafficType for congestion detail,
282+ * speedManagementType, …). Without this, those records fell back to the generic
283+ * record class name and lost their specific kind.
284+ */
285+ function subtypeOf ( rec : XmlObject ) : string | undefined {
239286 return (
240287 getXmlChildText ( getXmlChild ( rec , "cause" ) , "causeType" ) ??
241- getXmlChildText ( rec , "vehicleObstructionType" )
288+ getXmlChildText ( rec , "accidentType" ) ??
289+ getXmlChildText ( rec , "obstructionType" ) ??
290+ getXmlChildText ( rec , "environmentalObstructionType" ) ??
291+ getXmlChildText ( rec , "vehicleObstructionType" ) ??
292+ getXmlChildText ( rec , "generalNetworkManagementType" ) ??
293+ getXmlChildText ( rec , "abnormalTrafficType" ) ??
294+ getXmlChildText ( rec , "speedManagementType" )
242295 ) ;
243296}
244297
298+ /** Human cause text (DATEX `cause/causeDescription`), a multilingual block. */
299+ function causeDescriptionOf ( rec : XmlObject ) : string | undefined {
300+ return multilingual ( getXmlChild ( getXmlChild ( rec , "cause" ) , "causeDescription" ) , "en" ) ;
301+ }
302+
245303function speedLimitOf ( rec : XmlObject ) : number | undefined {
246304 const raw = getXmlChildText ( rec , "temporarySpeedLimit" ) ;
247305 if ( raw == null ) return undefined ;
@@ -359,7 +417,8 @@ function collectOpenLr(rec: XmlObject): string | undefined {
359417 return getXmlChildText ( rec , "openlrBinary" ) ?? undefined ;
360418}
361419
362- function collectRefs ( rec : XmlObject ) : RoadEvent [ "externalRefs" ] {
420+ /** Alert-C/TMC reference (country + table + primary specific-location code). */
421+ function tmcOf ( rec : XmlObject ) : NonNullable < RoadEvent [ "externalRefs" ] > [ "tmc" ] | undefined {
363422 const locRef = getXmlChild ( rec , "locationReference" ) ;
364423 if ( ! locRef ) return undefined ;
365424
@@ -374,11 +433,26 @@ function collectRefs(rec: XmlObject): RoadEvent["externalRefs"] {
374433 const table = getXmlChildText ( alertC , "alertCLocationTableNumber" ) ;
375434 const code = getXmlChildText ( getXmlChild ( primary , "alertCLocation" ) , "specificLocation" ) ;
376435 if ( country && table && code ) {
377- return { tmc : { country, table : parseFloat ( table ) , code : parseInt ( code , 10 ) } } ;
436+ return { country, table : parseFloat ( table ) , code : parseInt ( code , 10 ) } ;
378437 }
379438 return undefined ;
380439}
381440
441+ /** Provider external location code (NDW's `externalReferencing`, e.g. RIS-index). */
442+ function externalLocationOf ( rec : XmlObject ) : { system : string ; code : string } | undefined {
443+ const er = getXmlChild ( getXmlChild ( rec , "locationReference" ) , "externalReferencing" ) ;
444+ const system = getXmlChildText ( er , "externalReferencingSystem" ) ;
445+ const code = getXmlChildText ( er , "externalLocationCode" ) ;
446+ return system && code ? { system, code } : undefined ;
447+ }
448+
449+ function externalRefsOf ( rec : XmlObject ) : RoadEvent [ "externalRefs" ] {
450+ const tmc = tmcOf ( rec ) ;
451+ const external = externalLocationOf ( rec ) ;
452+ if ( ! tmc && ! external ) return undefined ;
453+ return { ...( tmc ? { tmc } : { } ) , ...( external ? { external } : { } ) } ;
454+ }
455+
382456interface SituationRecord {
383457 rec : XmlObject ;
384458 situationSeverity : string ;
@@ -506,6 +580,7 @@ export function parseDatexSituations(
506580
507581 const publicComment = getXmlChild ( rec , "generalPublicComment" ) ;
508582 const fallbackComment = getXmlChild ( rec , "comment" ) ;
583+ const causeDesc = causeDescriptionOf ( rec ) ;
509584
510585 const shared = {
511586 id : `${ src . id } :${ recId ( rec ) } ` ,
@@ -514,7 +589,7 @@ export function parseDatexSituations(
514589 domain : "roads" as const ,
515590 kind : "event" as const ,
516591 type,
517- subtype : causeOf ( rec ) ?? recType ?? undefined ,
592+ subtype : subtypeOf ( rec ) ?? recType ?? undefined ,
518593 category,
519594 isPlanned,
520595 ...severityFields ,
@@ -528,16 +603,22 @@ export function parseDatexSituations(
528603 restrictions : dimensionRestrictionsOf ( rec ) ,
529604 vehiclesAffected : vehiclesAffectedOf ( rec ) ,
530605 detour : detourOf ( rec ) ,
606+ detourGeometry : detourGeometryOf ( rec ) ,
531607 delaySeconds : leafNumber ( rec , "delayTimeValue" ) ,
532608 queueLengthMeters : leafNumber ( rec , "queueLength" ) ,
533609 relatedIds : relatedRefsOf ( rec ) ,
534610 sourceRaw : rec ,
535611 headline :
536612 multilingual ( publicComment , "en" ) ??
537613 multilingual ( fallbackComment , "en" ) ??
614+ causeDesc ??
538615 defaultHeadline ( type ) ,
539616 description :
540- multilingual ( publicComment , "en" ) ?? multilingual ( fallbackComment , "en" ) ?? undefined ,
617+ multilingual ( publicComment , "en" ) ??
618+ multilingual ( fallbackComment , "en" ) ??
619+ causeDesc ??
620+ undefined ,
621+ schedule : scheduleOf ( timeSpec ) ,
541622 validFrom : text ( timeSpec ?. [ "overallStartTime" ] ) ?? null ,
542623 validTo : text ( timeSpec ?. [ "overallEndTime" ] ) ?? null ,
543624 origin : {
@@ -557,14 +638,14 @@ export function parseDatexSituations(
557638 withGeom . push ( {
558639 ...shared ,
559640 geometry,
560- externalRefs : collectRefs ( rec ) ,
641+ externalRefs : externalRefsOf ( rec ) ,
561642 } ) ;
562643 } else {
563644 // openlr is defined here because we checked !geometry && !openlr above.
564645 unresolved . push ( {
565646 ...shared ,
566647 geometry : undefined ,
567- externalRefs : { ...collectRefs ( rec ) , openlr : openlr ! } ,
648+ externalRefs : { ...externalRefsOf ( rec ) , openlr : openlr ! } ,
568649 } ) ;
569650 }
570651 }
0 commit comments