plexus-python-pulse-common 1.1.75__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.
@@ -0,0 +1,6 @@
1
+ import importlib.metadata
2
+
3
+ try:
4
+ __version__ = importlib.metadata.version("plexus-python-pulse-common")
5
+ except importlib.metadata.PackageNotFoundError:
6
+ __version__ = "unknown"
@@ -0,0 +1,259 @@
1
+ from typing import Any, Self
2
+
3
+ import lxml.etree
4
+
5
+ from plexus.pulse.common.carto.OSMNode import OSMNode
6
+ from plexus.pulse.common.carto.OSMTags import OSMTags
7
+ from plexus.pulse.common.carto.OSMWay import OSMWay
8
+ from plexus.pulse.common.utils.gisutils import Coord, Proj
9
+
10
+
11
+ class OSMFile(object):
12
+ @classmethod
13
+ def load_file(cls, path: str, proj: Proj | None) -> Self:
14
+ """
15
+ Loads OSM from file given the path
16
+
17
+ :param path: path of OSM file
18
+ :param proj: cartographic projection
19
+
20
+ :return: an OSM file instance
21
+ """
22
+ root = lxml.etree.parse(path).getroot()
23
+
24
+ nodes = {}
25
+ ways = {}
26
+ for elem in root:
27
+ if elem.attrib.get("action") == "delete":
28
+ continue
29
+
30
+ if elem.tag == "node":
31
+ # Parse an OSM node
32
+ node_id = int(elem.attrib["id"])
33
+ tags = {}
34
+ for child in elem:
35
+ if child.tag == "tag":
36
+ k = child.attrib["k"]
37
+ v = child.attrib["v"]
38
+ tags[k] = v
39
+
40
+ tags = OSMTags.from_any_tags(tags)
41
+ coord = Coord.from_latlon(
42
+ float(elem.attrib["lat"]),
43
+ float(elem.attrib["lon"]),
44
+ tags.getfloat("z", 0.0),
45
+ proj,
46
+ )
47
+
48
+ nodes[node_id] = OSMNode(node_id, coord, tags)
49
+
50
+ elif elem.tag == "way":
51
+ # Parse an OSM way
52
+ way_id = int(elem.attrib["id"])
53
+
54
+ tags = {}
55
+ node_ids = []
56
+ for child in elem:
57
+ if child.tag == "tag":
58
+ k = child.attrib["k"]
59
+ v = child.attrib["v"]
60
+ tags[k] = v
61
+ elif child.tag == "nd":
62
+ node_id = int(child.attrib.get("ref"))
63
+ node_ids.append(node_id)
64
+
65
+ tags = OSMTags.from_any_tags(tags)
66
+
67
+ ways[way_id] = OSMWay(way_id, node_ids, tags)
68
+
69
+ return OSMFile(nodes, ways)
70
+
71
+ @classmethod
72
+ def from_coords(cls, coords: list[Coord], way_tags: dict[str, Any] = None) -> Self:
73
+ """
74
+ Constructs a single-way OSM file from the given coords
75
+
76
+ :param coords: coords on a single way
77
+ :param way_tags: tags of the corresponding way
78
+
79
+ :return: OSM file contains one way
80
+ """
81
+ osm = OSMFile()
82
+ osm.add_way_from_coords(coords, way_tags or {})
83
+
84
+ return osm
85
+
86
+ @classmethod
87
+ def from_coords_list(cls, coords_list: list[list[Coord]], way_tags_list: list[dict[str, Any]] = None) -> Self:
88
+ """
89
+ Constructs a multi-way OSM file from the given coords
90
+
91
+ :param coords_list: coords of each way
92
+ :param way_tags_list: tags of each corresponding way
93
+
94
+ :return: OSM file contains multiple ways
95
+ """
96
+ osm = OSMFile()
97
+
98
+ if not way_tags_list or len(way_tags_list) != len(coords_list):
99
+ for coords in coords_list:
100
+ osm.add_way_from_coords(coords)
101
+ else:
102
+ for coords, way_tags in zip(coords_list, way_tags_list):
103
+ osm.add_way_from_coords(coords, way_tags or {})
104
+
105
+ return osm
106
+
107
+ def __init__(self, nodes: dict[int, OSMNode] = None, ways: dict[int, OSMWay] = None):
108
+ """
109
+ Represents an editable general purpose OSM file
110
+
111
+ :param nodes: nodes
112
+ :param ways: ways
113
+ """
114
+ self.nodes: dict[int, OSMNode] = {}
115
+ self.ways: dict[int, OSMWay] = {}
116
+
117
+ self.next_node_id = 1
118
+ self.next_way_id = 1
119
+
120
+ if nodes:
121
+ self.nodes = nodes
122
+ self.next_node_id = max(nodes.keys()) + 1
123
+ if ways:
124
+ self.ways = ways
125
+ self.next_way_id = max(ways.keys()) + 1
126
+
127
+ def add_node(self, node: OSMNode) -> OSMNode:
128
+ """
129
+ Adds the given node
130
+
131
+ :param node: the node to be added
132
+
133
+ :return: added node
134
+ """
135
+ self.nodes[node.node_id] = node
136
+ if node.node_id >= self.next_node_id:
137
+ self.next_node_id = node.node_id + 1
138
+ return node
139
+
140
+ def add_node_from_coord(self, coord: Coord, tags: dict[str, Any] = None) -> OSMNode:
141
+ """
142
+ Adds node given the coord and tags
143
+
144
+ :param coord: coord of the node
145
+ :param tags: tags of the node
146
+
147
+ :return: added node
148
+ """
149
+ return self.add_node(OSMNode(self.next_node_id, coord, OSMTags.from_any_tags(tags)))
150
+
151
+ def add_way(self, way: OSMWay) -> OSMWay:
152
+ """
153
+ Adds the given way
154
+
155
+ :param way: the way to be added
156
+
157
+ :return: added way
158
+ """
159
+ self.ways[way.way_id] = way
160
+ if way.way_id >= self.next_way_id:
161
+ self.next_way_id = way.way_id + 1
162
+ return way
163
+
164
+ def add_way_from_coords(self, coords: list[Coord], tags: dict[str, Any] = None) -> OSMWay:
165
+ """
166
+ Adds way given the coords of the nodes and tags
167
+
168
+ :param coords: coords of the nodes on the way
169
+ :param tags: tags of the way
170
+
171
+ :return: added way
172
+ """
173
+ return self.add_way_from_nodes(coords, tags)
174
+
175
+ def add_way_from_node_ids(self, node_ids: list[int], tags: dict[str, Any] = None) -> OSMWay:
176
+ """
177
+ Adds way given the ids of the nodes and tags
178
+
179
+ :param node_ids: ids of the nodes on the way
180
+ :param tags: tags of the way
181
+
182
+ :return: added way
183
+ """
184
+ return self.add_way_from_nodes(node_ids, tags)
185
+
186
+ def add_way_from_nodes(self, elements: list[OSMNode | Coord | int], tags: dict[str, Any] = None) -> OSMWay:
187
+ """
188
+ Adds way given list of the nodes, the coords, or the ids of the nodes and tags
189
+
190
+ :param elements: a list of the nodes, the coords, or the ids of the nodes on the way
191
+ :param tags: tags of the way
192
+
193
+ :return: added way
194
+ """
195
+ node_ids = []
196
+ for elem in elements:
197
+ if isinstance(elem, OSMNode):
198
+ node_ids.append(self.add_node_from_coord(elem.coord, elem.tags.tags).node_id)
199
+ elif isinstance(elem, Coord):
200
+ node_ids.append(self.add_node_from_coord(elem, {}).node_id)
201
+ else:
202
+ node_ids.append(elem)
203
+ return self.add_way(OSMWay(self.next_way_id, node_ids, OSMTags.from_any_tags(tags)))
204
+
205
+ def write(self, filename: str):
206
+ """
207
+ Writes this OSM file instance to the given file name
208
+
209
+ :param filename: file path to write into
210
+ """
211
+ root = lxml.etree.Element("osm")
212
+ root.attrib.update({
213
+ "version": "0.6",
214
+ "generator": "Plexus",
215
+ })
216
+
217
+ for _, node in self.nodes.items():
218
+ node_elem = lxml.etree.Element("node")
219
+ node_elem.attrib.update(
220
+ {
221
+ "id": str(node.node_id),
222
+ "lat": str(node.coord.lat),
223
+ "lon": str(node.coord.lon),
224
+ "version": "1",
225
+ }
226
+ )
227
+
228
+ for k, v in node.tags.items():
229
+ tag_elem = lxml.etree.Element("tag")
230
+ tag_elem.attrib.update({"k": str(k), "v": str(v)})
231
+ node_elem.append(tag_elem)
232
+
233
+ root.append(node_elem)
234
+
235
+ for _, way in self.ways.items():
236
+ way_elem = lxml.etree.Element("way")
237
+ way_elem.attrib.update(
238
+ {
239
+ "id": str(way.way_id),
240
+ "version": "1",
241
+ }
242
+ )
243
+
244
+ for k, v in way.tags.items():
245
+ tag_elem = lxml.etree.Element("tag")
246
+ tag_elem.attrib.update({"k": str(k), "v": str(v)})
247
+ way_elem.append(tag_elem)
248
+
249
+ for node_id in way.node_ids:
250
+ nd_elem = lxml.etree.Element("nd")
251
+ nd_elem.attrib.update({"ref": str(node_id)})
252
+ way_elem.append(nd_elem)
253
+
254
+ root.append(way_elem)
255
+
256
+ s = lxml.etree.tostring(root, pretty_print=True, encoding="unicode")
257
+
258
+ with open(filename, "w") as fh:
259
+ fh.write(s)
@@ -0,0 +1,25 @@
1
+ from plexus.common.utils.strutils import repr_data
2
+
3
+ from plexus.pulse.common.carto.OSMTags import OSMTags
4
+ from plexus.pulse.common.utils.gisutils import Coord
5
+
6
+
7
+ class OSMNode(object):
8
+ """
9
+ Represents node in OSM
10
+ """
11
+
12
+ def __init__(self, node_id: int, coord: Coord, tags: OSMTags):
13
+ """
14
+ Creates an instance from the given node id, coord, and tags
15
+
16
+ :param node_id: id
17
+ :param coord: node coordinate
18
+ :param tags: tags
19
+ """
20
+ self.node_id = node_id
21
+ self.coord = coord
22
+ self.tags = tags
23
+
24
+ def __str__(self):
25
+ return repr_data(self)
@@ -0,0 +1,101 @@
1
+ import datetime
2
+ from typing import Any, Self
3
+
4
+ from plexus.common.utils.strutils import repr_data, str_conv
5
+
6
+
7
+ class OSMTags(object):
8
+ """
9
+ Represents tag in OSM
10
+ """
11
+
12
+ @classmethod
13
+ def from_any_tags(cls, any_tags: dict[str, Any] = None) -> Self:
14
+ """
15
+ Creates an instance from any-typed tags by automatically inferring data types
16
+
17
+ :param any_tags: tags with any-typed values
18
+
19
+ :return: OSM tag instance
20
+ """
21
+ if any_tags is None:
22
+ return OSMTags({})
23
+ return OSMTags({k: str_conv(v) for k, v in any_tags.items()})
24
+
25
+ def __init__(self, tags: dict[str, Any]):
26
+ """
27
+ Creates an instance from the given tags
28
+
29
+ :param tags: given tags
30
+ """
31
+ self.tags = tags
32
+
33
+ def __str__(self):
34
+ return repr_data(self)
35
+
36
+ def __iter__(self):
37
+ return self.tags.__iter__()
38
+
39
+ def __getitem__(self, k: str):
40
+ return self.tags.__getitem__(k)
41
+
42
+ def items(self):
43
+ return self.tags.items()
44
+
45
+ def set(self, key: str, value: Any) -> Any:
46
+ old_value = self.tags.get(key)
47
+ self.tags[key] = str_conv(value)
48
+ return old_value
49
+
50
+ def get(self, key: str, default: Any | None = None) -> Any | None:
51
+ return self.tags.get(key, default)
52
+
53
+ def getint(self, key: str, default: int | None = None) -> int | None:
54
+ value = self.tags.get(key, default)
55
+ if isinstance(value, int) and not isinstance(value, bool):
56
+ return value
57
+ if default is None:
58
+ return None
59
+ if isinstance(default, int) and not isinstance(default, bool):
60
+ return default
61
+ raise TypeError("type of either the value or the given default is incompatible, should be 'int'")
62
+
63
+ def getfloat(self, key: str, default: float | None = None) -> float | None:
64
+ value = self.tags.get(key, default)
65
+ if isinstance(value, float):
66
+ return value
67
+ if default is None:
68
+ return None
69
+ if isinstance(default, float):
70
+ return default
71
+ raise TypeError("type of either the value or the given default is incompatible, should be 'float'")
72
+
73
+ def getboolean(self, key: str, default: bool | None = None) -> bool | None:
74
+ value = self.tags.get(key, default)
75
+ if isinstance(value, bool):
76
+ return value
77
+ if default is None:
78
+ return None
79
+ if isinstance(default, bool):
80
+ return default
81
+ raise TypeError("type of either the value or the given default is incompatible, should be 'bool'")
82
+
83
+ def getdatetime(self, key: str, default: datetime.datetime | None = None) -> datetime.datetime | None:
84
+ value = self.tags.get(key, default)
85
+ if isinstance(value, datetime.datetime):
86
+ return value
87
+ if default is None:
88
+ return None
89
+ if isinstance(default, datetime.datetime):
90
+ return default
91
+ raise TypeError("type of either the value or the given default is incompatible, should be 'datetime.datetime'")
92
+
93
+ def getstring(self, key: str, default: str | None = None) -> str | None:
94
+ value = self.tags.get(key, default)
95
+ if isinstance(value, str):
96
+ return value
97
+ if default is None:
98
+ return None
99
+ if isinstance(default, str):
100
+ return default
101
+ raise TypeError("type of either the value or the given default is incompatible, should be 'str'")
@@ -0,0 +1,24 @@
1
+ from plexus.common.utils.strutils import repr_data
2
+
3
+ from plexus.pulse.common.carto.OSMTags import OSMTags
4
+
5
+
6
+ class OSMWay(object):
7
+ """
8
+ Represents way in OSM
9
+ """
10
+
11
+ def __init__(self, way_id: int, node_ids: list[int], tags: OSMTags):
12
+ """
13
+ Creates an instance from the given way id, node ids, and tags
14
+
15
+ :param way_id: id
16
+ :param node_ids: nodes ids
17
+ :param tags: tags
18
+ """
19
+ self.way_id = way_id
20
+ self.node_ids = node_ids
21
+ self.tags = tags
22
+
23
+ def __str__(self):
24
+ return repr_data(self)
@@ -0,0 +1,11 @@
1
+ __all__ = [
2
+ "OSMFile",
3
+ "OSMNode",
4
+ "OSMTags",
5
+ "OSMWay",
6
+ ]
7
+
8
+ from plexus.pulse.common.carto.OSMFile import OSMFile
9
+ from plexus.pulse.common.carto.OSMNode import OSMNode
10
+ from plexus.pulse.common.carto.OSMTags import OSMTags
11
+ from plexus.pulse.common.carto.OSMWay import OSMWay
File without changes
@@ -0,0 +1,35 @@
1
+ from pathlib import Path
2
+
3
+ import markdown_it as mdit
4
+ import yaml
5
+ from plexus.common.utils.funcutils import singleton
6
+ from plexus.common.utils.jsonutils import JsonObject
7
+
8
+ __all__ = [
9
+ "predefined_tagset_specs",
10
+ ]
11
+
12
+
13
+ @singleton
14
+ def predefined_tagset_specs() -> list[tuple[Path, JsonObject]]:
15
+ # Register simple constructors for !markdown tags yaml.load can parse them.
16
+ def markdown_constructor(loader: yaml.Loader, node: yaml.ScalarNode | yaml.MappingNode):
17
+ text = loader.construct_scalar(node)
18
+ tokens = mdit.MarkdownIt().parse(text)
19
+ for token in tokens:
20
+ if token.type.startswith("heading"):
21
+ raise ValueError("markdown content in tagset specifications cannot contain headings.")
22
+ return {
23
+ "type": "markdown",
24
+ "text": text,
25
+ }
26
+
27
+ yaml.SafeLoader.add_constructor("!markdown", markdown_constructor)
28
+
29
+ def load():
30
+ root = Path(__file__).parent
31
+ for path in root.glob("**/*.tagset.yaml"):
32
+ text = path.read_text(encoding="utf-8")
33
+ yield path, yaml.load(text, yaml.SafeLoader)
34
+
35
+ return list(load())
@@ -0,0 +1,98 @@
1
+ $namespace: "unittest"
2
+ $version: "1.0.0"
3
+ # language=markdown
4
+ $desc: !markdown |
5
+ **Unit Test Tagset**
6
+
7
+ A collection of tags designed for annotating and categorizing data in unit tests. This tagset includes categories
8
+ for camera views, radar views, LiDAR views, environmental conditions, and roadway characteristics. Each tag is
9
+ defined with a description that provides context for its use in unit testing scenarios, helping to ensure that
10
+ test cases are accurately annotated and can be easily understood by developers and testers.
11
+ $def:
12
+ anchored_tags:
13
+ &anchored_tags
14
+ anchored:
15
+ # language=markdown
16
+ $desc: !markdown |
17
+ Anchored tags that describe specific perspectives or viewpoints from which data is captured. These tags are
18
+ typically associated with sensor data and provide critical context for understanding the scene and the
19
+ information being analyzed. For example, camera views may include front center, front left, front right, side
20
+ left, side right, rear left, and rear right perspectives, while radar and LiDAR views may include unified,
21
+ bumper center, bumper left, bumper right, and similar perspectives.
22
+ $schema:
23
+ type: object
24
+ properties:
25
+ dummy_property:
26
+ description: "This is a dummy property that serves as a placeholder for the actual properties that would be defined under the anchored tags."
27
+ type: string
28
+ additionalProperties: true
29
+ another_anchored:
30
+ # language=markdown
31
+ $desc: !markdown |
32
+ Another set of anchored tags that describe specific perspectives or viewpoints from which data is captured.
33
+ These tags may be used in different contexts or for different types of sensors, but they serve a similar
34
+ purpose in providing critical context for understanding the scene and the information being analyzed.
35
+ $tags:
36
+ level_1_tag:
37
+ $desc: "This is a level 1 tag that provides a high-level categorization for the data."
38
+ $schema:
39
+ type: object
40
+ properties:
41
+ dummy_property:
42
+ description: "This is a dummy property that serves as a placeholder for the actual properties that would be defined under this level 1 tag."
43
+ type: string
44
+ additionalProperties: true
45
+ level_2_tag:
46
+ $desc: "This is a level 2 tag that provides a more specific categorization for the data."
47
+ level_3_tag: { $desc: "level 3 tag description", <<: *anchored_tags }
48
+ another_level_3_tag: { $desc: "another level 3 tag description" }
49
+ another_level_2_tag:
50
+ $desc: "This is another level 2 tag that provides a different specific categorization for the data."
51
+ $schema:
52
+ type: object
53
+ properties:
54
+ dummy_property:
55
+ description: "This is a dummy property that serves as a placeholder for the actual properties that would be defined under this level 2 tag."
56
+ type: object
57
+ properties:
58
+ dummy_sub_property:
59
+ description: "This is a dummy sub-property that serves as a placeholder for the actual sub-properties that would be defined under this dummy property."
60
+ type: string
61
+ additionalProperties: false
62
+ required: [ dummy_property ]
63
+ additionalProperties: true
64
+ <<: *anchored_tags
65
+ another_level_1_tag:
66
+ # language=markdown
67
+ $desc: !markdown |
68
+ This is another level 1 tag that provides a different high-level categorization for the data. It may be used
69
+ to group data based on different criteria or characteristics, such as environmental conditions, roadway
70
+ characteristics, or other relevant factors that are important for unit testing scenarios.
71
+ $schema:
72
+ type: object
73
+ properties:
74
+ dummy_elements:
75
+ description: "This is a dummy array property that serves as a placeholder for the actual array properties that would be defined under this level 1 tag."
76
+ type: array
77
+ items:
78
+ type: integer
79
+ minimum: 0
80
+ minItems: 1
81
+ maxItems: 3
82
+ required: [ dummy_elements ]
83
+ additionalProperties: true
84
+ level_2_tag:
85
+ # language=markdown
86
+ $desc: !markdown |
87
+ This is a level 2 tag that provides a more specific categorization for the data under the second level 1 tag.
88
+ It may be used to further refine the categorization based on specific criteria or characteristics relevant to
89
+ the second level 1 tag, such as different types of environmental conditions or roadway characteristics.
90
+ level_3_tag:
91
+ # language=markdown
92
+ $desc: !markdown |
93
+ This is a level 3 tag that provides an even more specific categorization for the data under the level 2 tag
94
+ of the second level 1 tag. It may be used to further refine the categorization based on very specific
95
+ criteria or characteristics relevant to the level 2 tag, such as specific weather conditions, road types,
96
+ or other detailed factors that are important for unit testing scenarios.
97
+ level_4_tag:
98
+ $desc: "This is a level 4 tag that provides an even more specific categorization for the data under the level 3 tag. Actually, you can have as many levels as you want, but for the sake of this example, we will stop at level 4."