datex2-xml2json 0.0.1__py3-none-any.whl

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.
Files changed (65) hide show
  1. datex2_xml2json/__init__.py +0 -0
  2. datex2_xml2json/json_validator.py +36 -0
  3. datex2_xml2json/merge.py +39 -0
  4. datex2_xml2json/schema/Baustellen-4-00-00.sch +7 -0
  5. datex2_xml2json/schema/DATEXIISchema_2_2_0.xsd +4707 -0
  6. datex2_xml2json/schema/DATEXIISchema_2_2_3.xsd +6783 -0
  7. datex2_xml2json/schema/DATEXII_3.7_Profile/.DS_Store +0 -0
  8. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_AfirEnergyInfrastructure.json +2455 -0
  9. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_AfirFacilities.json +2809 -0
  10. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Common.json +3651 -0
  11. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_CommonExtension.json +919 -0
  12. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_ControlledZone.json +1070 -0
  13. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_D2Payload.json +11 -0
  14. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_EnergyInfrastructure.json +1924 -0
  15. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Facilities.json +2858 -0
  16. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_FaultAndStatus.json +1415 -0
  17. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_LocationExtension.json +186 -0
  18. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_LocationReferencing.json +4847 -0
  19. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Parking.json +5314 -0
  20. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_ReroutingManagementEnhanced.json +1160 -0
  21. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_RoadTrafficData.json +1938 -0
  22. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Situation.json +8040 -0
  23. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_TrafficManagementPlan.json +2358 -0
  24. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_TrafficRegulation.json +3377 -0
  25. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_UrbanExtensions.json +194 -0
  26. datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Vms.json +2544 -0
  27. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/.DS_Store +0 -0
  28. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_AfirEnergyInfrastructure.xsd +2102 -0
  29. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_AfirFacilities.xsd +2060 -0
  30. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Common.xsd +4152 -0
  31. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_CommonExtension.xsd +1102 -0
  32. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_ControlledZone.xsd +601 -0
  33. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_D2Payload.xsd +280 -0
  34. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_EnergyInfrastructure.xsd +1761 -0
  35. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Facilities.xsd +2037 -0
  36. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_FaultAndStatus.xsd +1076 -0
  37. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_LocationExtension.xsd +196 -0
  38. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_LocationReferencing.xsd +4132 -0
  39. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Parking.xsd +4025 -0
  40. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_ReroutingManagementEnhanced.xsd +997 -0
  41. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_RoadTrafficData.xsd +1273 -0
  42. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Situation.xsd +5777 -0
  43. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_TrafficManagementPlan.xsd +1184 -0
  44. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_TrafficRegulation.xsd +2611 -0
  45. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_UrbanExtensions.xsd +148 -0
  46. datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Vms.xsd +2290 -0
  47. datex2_xml2json/schema/MDM_BaustellenInfoDigitalRoadworksProfile_04-03-00.xsd +2401 -0
  48. datex2_xml2json/schema/MDM_Datenmodell_Baustellen_04-00-00.xsd +3232 -0
  49. datex2_xml2json/schema/cifs_schema.json +161 -0
  50. datex2_xml2json/transform/.DS_Store +0 -0
  51. datex2_xml2json/transform/__init__.py +13 -0
  52. datex2_xml2json/transform/base.py +79 -0
  53. datex2_xml2json/transform/cifs_transformer.py +275 -0
  54. datex2_xml2json/transform/geojson_geometry_transformer.py +159 -0
  55. datex2_xml2json/transform/geoparquet_transformer.py +160 -0
  56. datex2_xml2json/transform/jsonata_transformer.py +100 -0
  57. datex2_xml2json/transform/roads_regio_transformer.py +88 -0
  58. datex2_xml2json/transform/source_setter.py +42 -0
  59. datex2_xml2json/transform/transformation_rules.py +526 -0
  60. datex2_xml2json/transformer.py +197 -0
  61. datex2_xml2json/util.py +141 -0
  62. datex2_xml2json-0.0.1.dist-info/METADATA +176 -0
  63. datex2_xml2json-0.0.1.dist-info/RECORD +65 -0
  64. datex2_xml2json-0.0.1.dist-info/WHEEL +4 -0
  65. datex2_xml2json-0.0.1.dist-info/licenses/LICENSE.md +179 -0
@@ -0,0 +1,161 @@
1
+ {
2
+ "$schema": "urn:json-schema:2020-12",
3
+ "type": "object",
4
+ "properties": {
5
+ "incidents": {
6
+ "type": "array",
7
+ "items": {
8
+ "type": "object",
9
+ "properties": {
10
+ "id": {
11
+ "type": "string",
12
+ "description": "Specifies an alphanumeric or numeric identifier. The ID must be globally unique to your feed and remain stable over an incident’s lifetime.",
13
+ "minLength": 3
14
+ },
15
+ "parent_event": {
16
+ "type": "string",
17
+ "description": "For events with multiple closures such as marathons or festivals, you can associate the incident to an event object."
18
+ },
19
+ "creationtime": {
20
+ "type": "string",
21
+ "description": "Datetime when the incident was created.",
22
+ "format": "date-time"
23
+ },
24
+ "updatetime": {
25
+ "type": "string",
26
+ "description": "Datetime when the incident was last updated.",
27
+ "format": "date-time"
28
+ },
29
+ "source": {
30
+ "type": "object",
31
+ "description": "Encapsulates the elements that specify the source of information for a single incident.",
32
+ "properties": {
33
+ "reference": {
34
+ "type": "string",
35
+ "description": "Uses a reference ID to identify the organization that sourced the information.",
36
+ "minLength": 1
37
+ },
38
+ "name": {
39
+ "type": "string",
40
+ "description": "Identifies the organization from which the information was sourced, by the organization’s name.",
41
+ "minLength": 1
42
+ },
43
+ "url": {
44
+ "type": "string",
45
+ "description": "Specifies the URL for the organization from which the information was sourced.",
46
+ "minLength": 1
47
+ }
48
+ },
49
+ "required": [
50
+ "reference"
51
+ ]
52
+ },
53
+ "type": {
54
+ "description": "Specifies the type of incident.",
55
+ "enum": [
56
+ "ACCIDENT",
57
+ "CONSTRUCTION",
58
+ "HAZARD",
59
+ "ROAD_CLOSED",
60
+ "POLICE",
61
+ "CHIT_CHAT",
62
+ "JAM"
63
+ ]
64
+ },
65
+ "subtype": {
66
+ "description": "Further refinement of the information in the \"type\" element.",
67
+ "enum": [
68
+ "",
69
+ "ACCIDENT_MAJOR",
70
+ "ACCIDENT_MINOR",
71
+ "HAZARD_ON_ROAD",
72
+ "HAZARD_ON_ROAD_CAR_STOPPED",
73
+ "HAZARD_ON_ROAD_CONSTRUCTION",
74
+ "HAZARD_ON_ROAD_ICE",
75
+ "HAZARD_ON_ROAD_LANE_CLOSED",
76
+ "HAZARD_ON_ROAD_OBJECT",
77
+ "HAZARD_ON_ROAD_OIL",
78
+ "HAZARD_ON_ROAD_POT_HOLE",
79
+ "HAZARD_ON_ROAD_ROAD_KILL",
80
+ "HAZARD_ON_ROAD_TRAFFIC_LIGHT_FAULT",
81
+ "HAZARD_ON_SHOULDER",
82
+ "HAZARD_ON_SHOULDER_ANIMALS",
83
+ "HAZARD_ON_SHOULDER_CAR_STOPPED",
84
+ "HAZARD_ON_SHOULDER_MISSING_SIGN",
85
+ "HAZARD_WEATHER",
86
+ "HAZARD_WEATHER_FLOOD",
87
+ "HAZARD_WEATHER_FOG",
88
+ "HAZARD_WEATHER_FREEZING_RAIN",
89
+ "HAZARD_WEATHER_HAIL",
90
+ "HAZARD_WEATHER_HEAT_WAVE",
91
+ "HAZARD_WEATHER_HEAVY_RAIN",
92
+ "HAZARD_WEATHER_HEAVY_SNOW",
93
+ "HAZARD_WEATHER_HURRICANE",
94
+ "HAZARD_WEATHER_MONSOON",
95
+ "HAZARD_WEATHER_TORNADO",
96
+ "JAM_HEAVY_TRAFFIC",
97
+ "JAM_LIGHT_TRAFFIC",
98
+ "JAM_MODERATE_TRAFFIC",
99
+ "JAM_STAND_STILL_TRAFFIC",
100
+ "ROAD_CLOSED_CONSTRUCTION",
101
+ "ROAD_CLOSED_EVENT",
102
+ "ROAD_CLOSED_HAZARD",
103
+ "POLICE_VISIBLE",
104
+ "POLICE_HIDING"
105
+ ]
106
+ },
107
+ "description": {
108
+ "type": "string",
109
+ "description": "Describes the incident, including the possible cause and consequences of the disruption.",
110
+ "minLength": 1
111
+ },
112
+ "street": {
113
+ "type": "string",
114
+ "description": "Specifies the name of the street on which the incident is occurring.",
115
+ "minLength": 2
116
+ },
117
+
118
+ "polyline": {
119
+ "type": "string",
120
+ "description": "Specifies the WGS84 latitude/longitude coordinates that describe the location of the incident. The decimal value should have at least 6 digits (to ensure accuracy of 0.11m or better). For any incident, you must submit at least two coordinates."
121
+ },
122
+ "direction": {
123
+ "description": "Specifies whether the disruption or closure affects one or both sides of the road.",
124
+ "enum": [
125
+ "ONE_DIRECTION",
126
+ "BOTH_DIRECTIONS"
127
+ ]
128
+ },
129
+ "starttime": {
130
+ "type": "string",
131
+ "description": "Specifies the start datetime for the period of disruption.",
132
+ "format": "date-time"
133
+ },
134
+ "endtime": {
135
+ "type": "string",
136
+ "description": "Specifies the end datetime for the period of disruption.",
137
+ "format": "date-time"
138
+ }
139
+ },
140
+ "additionalProperties": true,
141
+ "required": [
142
+ "id",
143
+ "description",
144
+ "starttime",
145
+ "street",
146
+ "type",
147
+ "polyline"
148
+ ]
149
+ }
150
+ },
151
+ "timestamp": {
152
+ "type": "string",
153
+ "description": "Specifies the creation time of the feed.",
154
+ "format": "date-time"
155
+ }
156
+ },
157
+ "additionalProperties": true,
158
+ "required": [
159
+ "incidents"
160
+ ]
161
+ }
Binary file
@@ -0,0 +1,13 @@
1
+ from .base import (
2
+ TransformerPipeline as TransformerPipeline,
3
+ RulesTransformer as RulesTransformer,
4
+ )
5
+ from .jsonata_transformer import JsonataTransformer as JsonataTransformer
6
+ from .transformation_rules import TransformationRuleFactory as TransformationRuleFactory
7
+ from .roads_regio_transformer import RoadsRegioTransformer as RoadsRegioTransformer
8
+ from .geojson_geometry_transformer import (
9
+ GeoJSONGeometryTransformer as GeoJSONGeometryTransformer,
10
+ )
11
+ from .geoparquet_transformer import GeoparquetRenderer as GeoparquetRenderer
12
+ from .cifs_transformer import CifsTransformer
13
+ from .source_setter import SourceSetter
@@ -0,0 +1,79 @@
1
+ from datex2_xml2json.util import replace, xpath_dict
2
+
3
+ from .transformation_rules import TransformationRules
4
+
5
+
6
+ class AbstractTransformer:
7
+ def transform(self, a_dict: dict) -> dict:
8
+ return a_dict
9
+
10
+
11
+ class TransformerPipeline(AbstractTransformer):
12
+ def __init__(self, transformers: list[AbstractTransformer]):
13
+ self.transformers = list(transformers)
14
+
15
+ def add_transformer(self, transformer: AbstractTransformer):
16
+ self.transformers.append(transformer)
17
+ return self
18
+
19
+ def transform(self, a_dict: dict) -> dict:
20
+ result = a_dict
21
+ for t in self.transformers:
22
+ result = t.transform(result)
23
+
24
+ return result
25
+
26
+
27
+ class RulesTransformer(AbstractTransformer):
28
+ def __init__(self, rules: TransformationRules):
29
+ self.rules = rules
30
+
31
+ def transform(self, a_dict: dict) -> dict:
32
+ # Transforms the result of xml2json transformation into DATEXIIv3 json.
33
+ # This includes:
34
+ # * renamings of certain properties according to v2 to v3 name changes
35
+ # * renamings of certain properties according to changed writing in json
36
+ # * casts values of certain properties to intended types
37
+ # * ...
38
+
39
+ return replace(
40
+ a_dict,
41
+ None,
42
+ self.rules.renamings,
43
+ self.rules.value_transforms,
44
+ self.rules.keys_to_drop,
45
+ self.rules.type_mappings,
46
+ self.rules.keys_to_pull_up,
47
+ )
48
+
49
+
50
+ class ProcessingTransformer(AbstractTransformer):
51
+ def __init__(self, xpath: str):
52
+ self.xpath = xpath
53
+
54
+ def transform(self, a_dict: dict) -> dict:
55
+ for elem in xpath_dict(a_dict, self.xpath):
56
+ self.process(elem)
57
+
58
+ return a_dict
59
+
60
+ def process(self, a_dict: dict):
61
+ pass
62
+
63
+
64
+ class SituationRecordTransformer(ProcessingTransformer):
65
+ def __init__(self, xpath: str):
66
+ super().__init__(xpath)
67
+
68
+ def process_situation_record(self, situation_record: dict) -> None:
69
+ pass
70
+
71
+ def process(self, situation_record_wrapper: dict) -> None:
72
+ if isinstance(situation_record_wrapper, dict) and len(situation_record_wrapper) == 1:
73
+ situation_record = situation_record_wrapper.get(next(iter(situation_record_wrapper.keys())))
74
+ else:
75
+ situation_record = situation_record_wrapper
76
+ if situation_record is None:
77
+ return
78
+
79
+ self.process_situation_record(situation_record)
@@ -0,0 +1,275 @@
1
+ import copy
2
+ import logging
3
+ import re
4
+
5
+ from .jsonata_transformer import JsonataTransformer
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ # TODO: PublicEvent notificcations and GeneralInstructionOrMessageToRoadUsers do not really match
10
+ # TODO how can we deduce direction for NRW incidents
11
+ cifs_expression = """
12
+ {
13
+ "incidents": features[
14
+ properties.type!="CarParks" and
15
+ properties.type!="ReroutingManagement" and
16
+ properties.type!="EquipmentOrSystemFault" and
17
+ properties.validityStatus!="suspended"
18
+ ][].{
19
+ "id": properties.id,
20
+ "creationtime": properties.situationRecordCreationTime,
21
+ "updatetime": properties.situationRecordVersionTime,
22
+ "description": properties.roadworksName ? properties.roadworksName : properties.description ?? "-",
23
+ "starttime": properties.overallStartTime,
24
+ "endtime": properties.overallEndTime,
25
+ "direction": "ONE_DIRECTION",
26
+ "street": $trim(properties.roadNumber?($contains(properties.roadName, properties.roadNumber)?
27
+ properties.roadName: properties.roadNumber & " " & properties.roadName): $split(properties.description, "\n")[0] ??
28
+ "Unbekannte StraĂŸe"),
29
+ "type": (
30
+ $typeBySubtypeLookup := {
31
+ "carriagewayClosures": "ROAD_CLOSED",
32
+ "closedPermanentlyForTheWinter": "ROAD_CLOSED",
33
+ "overnightClosures": "ROAD_CLOSED",
34
+ "roadClosed": "ROAD_CLOSED",
35
+ "unprotectedAccidentArea": "ACCIDENT"
36
+ };
37
+ $typeLookup := {
38
+ "Accident": "ACCIDENT",
39
+ "AnimalPresence": "HAZARD",
40
+ "AnimalPresenceObstruction": "HAZARD",
41
+ "AbnormalTraffic": "JAM",
42
+ "AuthorityOperation": "HAZARD",
43
+ "Conditions": "HAZARD",
44
+ "ConstructionWorks": "CONSTRUCTION",
45
+ "EnvironmentalObstruction": "HAZARD",
46
+ "GeneralObstruction": "HAZARD",
47
+ "GeneralNetworkManagement": "HAZARD",
48
+ "InfrastructureDamageObstruction": "HAZARD",
49
+ "MaintenanceWorks": "CONSTRUCTION",
50
+ "Obstruction": "HAZARD",
51
+ "RoadOrCarriagewayOrLaneManagement":"HAZARD",
52
+ "SpeedManagement": "HAZARD",
53
+ "VehicleObstruction": "HAZARD",
54
+ "PublicEvent": "CHIT_CHAT",
55
+ "GeneralInstructionOrMessageToRoadUsers": "CHIT_CHAT",
56
+ "NonWeatherRelatedRoadConditions": "HAZARD",
57
+ "WeatherRelatedRoadConditions": "HAZARD",
58
+ "DisturbanceActivity": "CHIT_CHAT",
59
+ "TransitInformation": "CHIT_CHAT"
60
+ };
61
+ $lookup($typeBySubtypeLookup , properties.situationRecordSubtype)?:$lookup($typeLookup , properties.type)?:"TODO"),
62
+ "subtype": (
63
+ $subtypeByTypeLookup := {
64
+ /* Accident (no subtypes, as DATEXII does not provide accident severity) */
65
+ /* AnimalPresence has no exact correspondance, we prefer HAZARD_ON_SHOULDER_ANIMALS over HAZARD_ON_ROAD_ROAD_KILL */
66
+ "AnimalPresence": "HAZARD_ON_SHOULDER_ANIMALS",
67
+ "AnimalPresenceObstruction": "HAZARD_ON_SHOULDER_ANIMALS",
68
+ "AuthorityOperation": "HAZARD_ON_ROAD",
69
+ "GeneralNetworkManagement": "HAZARD_ON_ROAD",
70
+ "InfrastructureDamageObstruction": "HAZARD_ON_ROAD",
71
+ "MaintenanceWorks": "HAZARD_ON_ROAD_CONSTRUCTION",
72
+ "Obstruction": "HAZARD_ON_ROAD",
73
+ "RoadOrCarriagewayOrLaneManagement": "HAZARD_ON_ROAD",
74
+ "VehicleObstruction": "HAZARD_ON_ROAD",
75
+ "WeatherRelatedRoadConditions": "HAZARD_WEATHER"
76
+ };
77
+ $subtypeBySubtypeLookup := {
78
+ /* AbnormalTraffic (no subtype for unspecifiedAbnormalTraffic and other)*/
79
+ "stationaryTraffic": "JAM_STAND_STILL_TRAFFIC",
80
+ "queuingTraffic": "JAM_HEAVY_TRAFFIC",
81
+ "slowTraffic": "JAM_MODERATE_TRAFFIC",
82
+ "heavyTraffic": "JAM_LIGHT_TRAFFIC",
83
+ /* Accident (no subtypes, as DATEXII does not provide accident severity) */
84
+ "roadworks": "HAZARD_ON_ROAD_CONSTRUCTION",
85
+ /* EnvironmentalObstruction*/
86
+ "avalanches": "HAZARD_WEATHER",
87
+ "earthquakeDamage": "HAZARD_ON_ROAD",
88
+ "fallenTrees": "HAZARD_ON_ROAD_OBJECT",
89
+ "fallingIce": "HAZARD_WEATHER_HAIL",
90
+ "fallingLightIceOrSnow": "HAZARD_WEATHER_FREEZING_RAIN",
91
+ "flashFloods": "HAZARD_WEATHER_MONSOON",
92
+ "flooding": "HAZARD_WEATHER_FLOOD",
93
+ "forestFire": "HAZARD_WEATHER",
94
+ "grassFire": "HAZARD_WEATHER",
95
+ "landslips": "HAZARD_ON_ROAD",
96
+ "mudSlide": "HAZARD_ON_ROAD",
97
+ "sewerOverflow": "HAZARD_ON_ROAD",
98
+ "rockfalls": "HAZARD_ON_ROAD",
99
+ "seriousFire": "HAZARD_WEATHER",
100
+ "smokeOrFumes": "HAZARD_WEATHER",
101
+ "stormDamage": "HAZARD_ON_ROAD",
102
+ "subsidence": "HAZARD_ON_ROAD",
103
+ /* GeneralNetworkManagement (default defined via subtypeByTypeLookup, no exceptions )*/
104
+ /* Obstruction (default defined via subtypeByTypeLookup, here only exceptions ) */
105
+ "objectOnTheRoad": "HAZARD_ON_ROAD_OBJECT",
106
+ "objectsFallingFromMovingVehicle": "HAZARD_ON_ROAD_OBJECT",
107
+ "obstructionOnTheRoad": "HAZARD_ON_ROAD_OBJECT",
108
+ /* RoadOrCarriagewayOrLaneManagement (default defined via subtypeByTypeLookup, here only exceptions ) */
109
+ "carriagewayClosures": "ROAD_CLOSED_CONSTRUCTION",/* TODO: subtype needs to be figured out via sibling situationRecord*/
110
+ "closedPermanentlyForTheWinter": "ROAD_CLOSED_HAZARD",
111
+ "doNotUseSpecifiedLanesOrCarriageways": "HAZARD_ON_ROAD_LANE_CLOSED",
112
+ "laneClosures": "HAZARD_ON_ROAD_LANE_CLOSED",
113
+ "overnightClosures": "ROAD_CLOSED_CONSTRUCTION",/* TODO: subtype needs to be figured out via sibling situationRecord*/
114
+ "roadClosed": "ROAD_CLOSED_CONSTRUCTION", /* TODO: subtype needs to be figured out via sibling situationRecord*/
115
+ /* VehicleObstruction (default defined via subtypeByTypeLookup, here only exceptions )*/
116
+ "abandonedVehicle": "HAZARD_ON_ROAD_CAR_STOPPED",
117
+ "brokenDownBus": "HAZARD_ON_ROAD_CAR_STOPPED",
118
+ "brokenDownHeavyLorry": "HAZARD_ON_ROAD_CAR_STOPPED",
119
+ "brokenDownVehicle": "HAZARD_ON_ROAD_CAR_STOPPED",
120
+ "damagedVehicle": "HAZARD_ON_ROAD_CAR_STOPPED",
121
+ "emergencyVehicle": "HAZARD_ON_ROAD_EMERGENCY_VEHICLE",
122
+ "unlitVehicleOnTheRoad": "HAZARD_ON_ROAD_CAR_STOPPED",
123
+ "vehicleOnFire": "HAZARD_ON_ROAD_CAR_STOPPED",
124
+ /* NonWeatherRelatedRoadConditions */
125
+ "roadSurfaceInPoorCondition": "HAZARD_ON_ROAD_POT_HOLE"
126
+ };
127
+ $lookup($subtypeBySubtypeLookup, properties.situationRecordSubtype)?:$lookup($subtypeByTypeLookup, properties.type)
128
+ ),
129
+ /* Note: As geometries might be LineStrings, Points or even MultiLineStrings, we just copy them and
130
+ handle them in a post_transform. Note: For Points, a zero-length polyline linestring will be created,
131
+ multilines will result in multiple CIFS events. */
132
+ "geometry": geometry
133
+ } ?? []
134
+ }
135
+ """
136
+
137
+
138
+ class CifsTransformer(JsonataTransformer):
139
+ # This pattern is used to exract the common ID prefix of all situation records which
140
+ # should be grouped in a single CIFS entry.
141
+ # Currently expected patterns are:
142
+ # 1. four hyphen separated numeric values, followed by no or any subsring
143
+ # 2. Any string/char prefix followed by dot + number
144
+ situation_record_group_prefix_regexp = r'^(\d+-\d\d\d\dV\d+)$|^(\d+-\d+-\d+-\d+-gegen).*|^(\d+-\d+-\d+-\d+).*|^(.*)\.\d+$'
145
+
146
+ def __init__(self, **kwargs):
147
+ super().__init__(cifs_expression)
148
+
149
+ def _group_by_common_situation_record_id_substring(self, incidents, pattern):
150
+ grouped_incidents = {}
151
+ p = re.compile(pattern)
152
+
153
+ for incident in incidents:
154
+ m = p.match(incident['id'])
155
+ if m is None:
156
+ logger.warning(f'No match for id {incident["id"]} with pattern {pattern} ')
157
+ continue
158
+ common_id_substring = next(group for group in m.groups() if group is not None)
159
+ if common_id_substring in grouped_incidents:
160
+ grouped_incidents[common_id_substring].append(incident)
161
+ else:
162
+ grouped_incidents[common_id_substring] = [incident]
163
+
164
+ return grouped_incidents
165
+
166
+ def _get_type_sub_type_prio(self, incident):
167
+ if incident.get('type') == 'ROAD_CLOSED':
168
+ return 10
169
+ if incident.get('subtype') == 'HAZARD_ON_ROAD_LANE_CLOSED':
170
+ return 4
171
+ if incident.get('subtype') == 'HAZARD_ON_ROAD_CONSTRUCTION':
172
+ return 3
173
+ if incident.get('subtype') == 'HAZARD_ON_ROAD' or incident.get('type') in ['CHIT_CHAT']:
174
+ return 1
175
+ else:
176
+ return 2
177
+
178
+ def _merge_incident_group(self, incident_group):
179
+ # Assumptions: besides type and subtype, all properties are assumed to be equals
180
+ # (TODO: we might check this explicitly)
181
+ #
182
+ # For now, all incidents are merged into one single incident which should represent all of them.
183
+ # Rules are as follows:
184
+ # * highest priority subtype/type wins
185
+ # TODO: subtype for road_closed to be set according to other incidents
186
+ # TODO: should jam be communicated in addition?
187
+ highest_prio = None
188
+ highest_prio_type = None
189
+ highest_prio_subtype = None
190
+ highest_prio_incident = None
191
+
192
+ for incident in incident_group:
193
+ current_prio = self._get_type_sub_type_prio(incident)
194
+ if highest_prio is None or current_prio > highest_prio:
195
+ logger.debug(
196
+ f'For {incident["id"]}, replace {highest_prio} {highest_prio_type} {highest_prio_subtype}'
197
+ f' by {incident["type"]} {incident.get("subtype")}'
198
+ )
199
+ highest_prio = current_prio
200
+ highest_prio_type = incident['type']
201
+ highest_prio_subtype = incident.get('subtype')
202
+ highest_prio_incident = incident
203
+ else:
204
+ logger.debug(
205
+ f'For {incident["id"]}, do not replace {highest_prio} {highest_prio_type} {highest_prio_subtype}'
206
+ f' by {incident["type"]} {incident.get("subtype")}'
207
+ )
208
+ return highest_prio_incident
209
+
210
+ def _transform_polyline_geom(self, linestring_coordinates) -> str:
211
+ """
212
+ Converts GeoJSON linestring coordinates array into CIFS polyline string.
213
+ The list of lon/lat coords (like [[10.0, 49.0], [11.0, 49.0]]) is converted into a
214
+ lat/lon ordered string ("49.0 10.0 49.0 11.0").
215
+ """
216
+ return ' '.join([f'{coords[1]} {coords[0]}' for coords in linestring_coordinates])
217
+
218
+ def _transform_incident_geometry(self, incident: dict) -> list[dict]:
219
+ """
220
+ Converts GeoJSON geometry of incident into a CIFS polyline.
221
+ As the GeoJSON geometry might be a MultiLineString, we return an array
222
+ of (in case of a multilinestring) multiple CIFS incidents with linear polylines.
223
+ Note: Point geometries will be converted into zero-length linestrings.
224
+ """
225
+ geometry = incident.pop('geometry')
226
+ geometry_type = geometry['type']
227
+ coordinates = geometry['coordinates']
228
+
229
+ if geometry_type == 'MultiLineString':
230
+ exploded_incidents = []
231
+ cnt = 0
232
+ for linestring_coordinates in coordinates:
233
+ cloned_incident = copy.deepcopy(incident)
234
+ cloned_incident['id'] = f'{cloned_incident["id"]}.{cnt:02d}'
235
+ cloned_incident['polyline'] = self._transform_polyline_geom(linestring_coordinates)
236
+ exploded_incidents.append(cloned_incident)
237
+ cnt += 1
238
+
239
+ return exploded_incidents
240
+
241
+ if geometry_type == 'Point':
242
+ incident['polyline'] = f'{coordinates[1]} {coordinates[0]}'
243
+ elif geometry_type == 'LineString':
244
+ incident['polyline'] = self._transform_polyline_geom(coordinates)
245
+ else:
246
+ logger.error('Unexpected geometry type %s for incident %s, incident is omitted', geometry_type, incident.get('id'))
247
+ return []
248
+
249
+ return [incident]
250
+
251
+ def _merge_grouped_incidents_and_handle_geometry(self, grouped_incidents):
252
+ incidents = []
253
+ for key in grouped_incidents:
254
+ try:
255
+ incident = self._merge_incident_group(grouped_incidents[key])
256
+ potentially_exploded_incidents = self._transform_incident_geometry(incident)
257
+ incidents.extend(potentially_exploded_incidents)
258
+ except Exception:
259
+ logger.exception('Error while merging for incidents of key %s', key)
260
+
261
+ return incidents
262
+
263
+ def post_transform(self, a_dict: dict) -> dict:
264
+ """
265
+ Retains the most expressive incident of a group of incidents describing
266
+ the same situation.
267
+ Currently, a common substring of the situationRecord IDs is used to group these events.
268
+ As this strategy is very brittle, other strategies should be considered, like
269
+ grouping by equivalent geometries/validity periods for situation records of the same situation.
270
+ """
271
+ grouped_incidents = self._group_by_common_situation_record_id_substring(
272
+ a_dict['incidents'], self.situation_record_group_prefix_regexp
273
+ )
274
+ a_dict['incidents'] = self._merge_grouped_incidents_and_handle_geometry(grouped_incidents)
275
+ return a_dict