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.
- datex2_xml2json/__init__.py +0 -0
- datex2_xml2json/json_validator.py +36 -0
- datex2_xml2json/merge.py +39 -0
- datex2_xml2json/schema/Baustellen-4-00-00.sch +7 -0
- datex2_xml2json/schema/DATEXIISchema_2_2_0.xsd +4707 -0
- datex2_xml2json/schema/DATEXIISchema_2_2_3.xsd +6783 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/.DS_Store +0 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_AfirEnergyInfrastructure.json +2455 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_AfirFacilities.json +2809 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Common.json +3651 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_CommonExtension.json +919 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_ControlledZone.json +1070 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_D2Payload.json +11 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_EnergyInfrastructure.json +1924 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Facilities.json +2858 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_FaultAndStatus.json +1415 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_LocationExtension.json +186 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_LocationReferencing.json +4847 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Parking.json +5314 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_ReroutingManagementEnhanced.json +1160 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_RoadTrafficData.json +1938 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Situation.json +8040 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_TrafficManagementPlan.json +2358 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_TrafficRegulation.json +3377 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_UrbanExtensions.json +194 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/DATEXII_3_Vms.json +2544 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/.DS_Store +0 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_AfirEnergyInfrastructure.xsd +2102 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_AfirFacilities.xsd +2060 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Common.xsd +4152 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_CommonExtension.xsd +1102 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_ControlledZone.xsd +601 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_D2Payload.xsd +280 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_EnergyInfrastructure.xsd +1761 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Facilities.xsd +2037 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_FaultAndStatus.xsd +1076 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_LocationExtension.xsd +196 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_LocationReferencing.xsd +4132 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Parking.xsd +4025 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_ReroutingManagementEnhanced.xsd +997 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_RoadTrafficData.xsd +1273 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Situation.xsd +5777 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_TrafficManagementPlan.xsd +1184 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_TrafficRegulation.xsd +2611 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_UrbanExtensions.xsd +148 -0
- datex2_xml2json/schema/DATEXII_3.7_Profile/xml/DATEXII_3_Vms.xsd +2290 -0
- datex2_xml2json/schema/MDM_BaustellenInfoDigitalRoadworksProfile_04-03-00.xsd +2401 -0
- datex2_xml2json/schema/MDM_Datenmodell_Baustellen_04-00-00.xsd +3232 -0
- datex2_xml2json/schema/cifs_schema.json +161 -0
- datex2_xml2json/transform/.DS_Store +0 -0
- datex2_xml2json/transform/__init__.py +13 -0
- datex2_xml2json/transform/base.py +79 -0
- datex2_xml2json/transform/cifs_transformer.py +275 -0
- datex2_xml2json/transform/geojson_geometry_transformer.py +159 -0
- datex2_xml2json/transform/geoparquet_transformer.py +160 -0
- datex2_xml2json/transform/jsonata_transformer.py +100 -0
- datex2_xml2json/transform/roads_regio_transformer.py +88 -0
- datex2_xml2json/transform/source_setter.py +42 -0
- datex2_xml2json/transform/transformation_rules.py +526 -0
- datex2_xml2json/transformer.py +197 -0
- datex2_xml2json/util.py +141 -0
- datex2_xml2json-0.0.1.dist-info/METADATA +176 -0
- datex2_xml2json-0.0.1.dist-info/RECORD +65 -0
- datex2_xml2json-0.0.1.dist-info/WHEEL +4 -0
- 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
|