plexus-python-common 1.0.7__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.
Potentially problematic release.
This version of plexus-python-common might be problematic. Click here for more details.
- plexus/common/__init__.py +6 -0
- plexus/common/carto/OSMFile.py +259 -0
- plexus/common/carto/OSMNode.py +25 -0
- plexus/common/carto/OSMTags.py +101 -0
- plexus/common/carto/OSMWay.py +24 -0
- plexus/common/carto/__init__.py +11 -0
- plexus/common/config.py +84 -0
- plexus/common/pose.py +107 -0
- plexus/common/proj.py +305 -0
- plexus/common/utils/__init__.py +0 -0
- plexus/common/utils/bagutils.py +218 -0
- plexus/common/utils/datautils.py +195 -0
- plexus/common/utils/jsonutils.py +92 -0
- plexus/common/utils/ormutils.py +335 -0
- plexus/common/utils/s3utils.py +118 -0
- plexus/common/utils/shutils.py +234 -0
- plexus/common/utils/strutils.py +285 -0
- plexus_python_common-1.0.7.dist-info/METADATA +29 -0
- plexus_python_common-1.0.7.dist-info/RECORD +21 -0
- plexus_python_common-1.0.7.dist-info/WHEEL +5 -0
- plexus_python_common-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
from typing import Any, Self
|
|
2
|
+
|
|
3
|
+
import lxml.etree
|
|
4
|
+
|
|
5
|
+
from plexus.common.carto.OSMNode import OSMNode
|
|
6
|
+
from plexus.common.carto.OSMTags import OSMTags
|
|
7
|
+
from plexus.common.carto.OSMWay import OSMWay
|
|
8
|
+
from plexus.common.proj 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 iker.common.utils.strutils import repr_data
|
|
2
|
+
|
|
3
|
+
from plexus.common.carto.OSMTags import OSMTags
|
|
4
|
+
from plexus.common.proj 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 iker.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 iker.common.utils.strutils import repr_data
|
|
2
|
+
|
|
3
|
+
from plexus.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.common.carto.OSMFile import OSMFile
|
|
9
|
+
from plexus.common.carto.OSMNode import OSMNode
|
|
10
|
+
from plexus.common.carto.OSMTags import OSMTags
|
|
11
|
+
from plexus.common.carto.OSMWay import OSMWay
|
plexus/common/config.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
|
|
3
|
+
from iker.common.utils import logger
|
|
4
|
+
from iker.common.utils.config import Config
|
|
5
|
+
from iker.common.utils.funcutils import singleton
|
|
6
|
+
from iker.common.utils.shutils import expanded_path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@singleton
|
|
10
|
+
def config() -> Config:
|
|
11
|
+
default_items: list[tuple[str, str, str]] = [
|
|
12
|
+
("pulse.commons", "logging.level", "INFO"),
|
|
13
|
+
("pulse.commons", "logging.format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s"),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
config = Config(expanded_path("~/.iker.pulse.cfg"))
|
|
17
|
+
config.restore()
|
|
18
|
+
config.update(default_items, overwrite=False)
|
|
19
|
+
|
|
20
|
+
return config
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def validate_pulse_super_token(token: str) -> bool:
|
|
24
|
+
"""
|
|
25
|
+
Validate the pulse super token.
|
|
26
|
+
:param token: The token to validate.
|
|
27
|
+
:return: True if the token is valid, False otherwise.
|
|
28
|
+
"""
|
|
29
|
+
return hashlib.sha256(token.encode()).hexdigest() == pulse_super_token_sha256()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def ask_pulse_super_token():
|
|
33
|
+
import getpass
|
|
34
|
+
super_token = getpass.getpass("Pulse super token required: ")
|
|
35
|
+
if not validate_pulse_super_token(super_token):
|
|
36
|
+
raise ValueError("wrong Pulse super token")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@singleton
|
|
40
|
+
def pulse_super_token_sha256() -> str:
|
|
41
|
+
return "d7f97abe12bb00a7d19cb5957350509c14b3b085cf84524b80d389c6c2086f1b"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def config_print_or_set(config: Config, section: str, key: str, value: str):
|
|
45
|
+
if value is not None:
|
|
46
|
+
if section is None or key is None:
|
|
47
|
+
raise ValueError("cannot specify value without section and key")
|
|
48
|
+
|
|
49
|
+
old_value = config.get(section, key)
|
|
50
|
+
config.set(section, key, value)
|
|
51
|
+
config.persist()
|
|
52
|
+
|
|
53
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
54
|
+
print(f"Section <{section}>")
|
|
55
|
+
print(f" {key} = {old_value} -> {value}")
|
|
56
|
+
|
|
57
|
+
else:
|
|
58
|
+
if section is None and key is None:
|
|
59
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
60
|
+
for section in config.config_parser.sections():
|
|
61
|
+
print(f"Section <{section}>")
|
|
62
|
+
for key, value in config.config_parser.items(section):
|
|
63
|
+
print(f" {key} = {value}")
|
|
64
|
+
|
|
65
|
+
elif section is not None and key is None:
|
|
66
|
+
if not config.has_section(section):
|
|
67
|
+
logger.warning("Configuration section <%s> not found", section)
|
|
68
|
+
return
|
|
69
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
70
|
+
print(f"Section <{section}>")
|
|
71
|
+
for key, value in config.config_parser.items(section):
|
|
72
|
+
print(f" {key} = {value}")
|
|
73
|
+
|
|
74
|
+
elif section is not None and key is not None:
|
|
75
|
+
value = config.get(section, key)
|
|
76
|
+
if value is None:
|
|
77
|
+
logger.warning("Configuration section <%s> key <%s> not found", section, key)
|
|
78
|
+
return
|
|
79
|
+
print(f"Configuration file '{config.config_path}'", )
|
|
80
|
+
print(f"Section <{section}>")
|
|
81
|
+
print(f" {key} = {value}")
|
|
82
|
+
|
|
83
|
+
else:
|
|
84
|
+
raise ValueError("cannot specify key without section")
|
plexus/common/pose.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from typing import Self
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pyquaternion as pyquat
|
|
5
|
+
from iker.common.utils.strutils import repr_data
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Pose(object):
|
|
9
|
+
@classmethod
|
|
10
|
+
def from_numbers(
|
|
11
|
+
cls,
|
|
12
|
+
px: float,
|
|
13
|
+
py: float,
|
|
14
|
+
pz: float,
|
|
15
|
+
qx: float,
|
|
16
|
+
qy: float,
|
|
17
|
+
qz: float,
|
|
18
|
+
qw: float,
|
|
19
|
+
ts: float = 0,
|
|
20
|
+
) -> Self:
|
|
21
|
+
"""
|
|
22
|
+
Constructs a pose from numbers representing position and orientation
|
|
23
|
+
"""
|
|
24
|
+
return Pose(ts, np.array([px, py, pz], dtype=np.float64), np.array([qw, qx, qy, qz], dtype=np.float64))
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def add(cls, x: Self, d: Self) -> Self:
|
|
28
|
+
"""
|
|
29
|
+
Performs pose SE3 addition, as x + d = y
|
|
30
|
+
"""
|
|
31
|
+
xq = pyquat.Quaternion(x.q)
|
|
32
|
+
dq = pyquat.Quaternion(d.q)
|
|
33
|
+
|
|
34
|
+
yp = x.p + xq.rotate(d.p)
|
|
35
|
+
yq = xq * dq
|
|
36
|
+
|
|
37
|
+
return Pose(0, yp, yq.normalised.elements)
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def sub(cls, y: Self, x: Self) -> Self:
|
|
41
|
+
"""
|
|
42
|
+
Performs pose SE3 subtraction, as x + d = y => d = y - x
|
|
43
|
+
"""
|
|
44
|
+
xq = pyquat.Quaternion(x.q)
|
|
45
|
+
yq = pyquat.Quaternion(y.q)
|
|
46
|
+
|
|
47
|
+
dp = xq.inverse.rotate(y.p - x.p)
|
|
48
|
+
dq = xq.inverse * yq
|
|
49
|
+
|
|
50
|
+
return Pose(0, dp, dq.normalised.elements)
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def interpolate(cls, a: Self, b: Self, t: float) -> Self:
|
|
54
|
+
"""
|
|
55
|
+
Interpolates between two given poses, as a * t + b * (1 - t)
|
|
56
|
+
|
|
57
|
+
:return: interpolated pose
|
|
58
|
+
"""
|
|
59
|
+
ts = a.ts + (b.ts - a.ts) * t
|
|
60
|
+
p = a.p + (b.p - a.p) * t
|
|
61
|
+
q = pyquat.Quaternion.slerp(pyquat.Quaternion(a.q), pyquat.Quaternion(b.q), t)
|
|
62
|
+
return Pose(ts, p, q.normalised.elements)
|
|
63
|
+
|
|
64
|
+
def __init__(self, ts: float, p: np.ndarray, q: np.ndarray):
|
|
65
|
+
"""
|
|
66
|
+
Represents a pose
|
|
67
|
+
|
|
68
|
+
:param ts: timestamp
|
|
69
|
+
:param p: position vector
|
|
70
|
+
:param q: orientation quaternion
|
|
71
|
+
"""
|
|
72
|
+
self.ts = ts
|
|
73
|
+
self.p = p
|
|
74
|
+
self.q = q
|
|
75
|
+
|
|
76
|
+
def matrix(self) -> np.ndarray:
|
|
77
|
+
"""
|
|
78
|
+
Returns the transformation matrix of this pose
|
|
79
|
+
|
|
80
|
+
:return: transformation matrix
|
|
81
|
+
"""
|
|
82
|
+
r = pyquat.Quaternion(self.q).rotation_matrix
|
|
83
|
+
t = self.p[:, None]
|
|
84
|
+
return np.block([[r, t], [np.zeros((1, 3), dtype=np.float64), np.ones((1, 1), dtype=np.float64)]])
|
|
85
|
+
|
|
86
|
+
def translate(self, v: np.ndarray) -> np.ndarray:
|
|
87
|
+
"""
|
|
88
|
+
Translate the given vector with the pose position
|
|
89
|
+
|
|
90
|
+
:param v: the vector to be translated
|
|
91
|
+
|
|
92
|
+
:return: translated vector
|
|
93
|
+
"""
|
|
94
|
+
return self.p + v
|
|
95
|
+
|
|
96
|
+
def rotate(self, v: np.ndarray) -> np.ndarray:
|
|
97
|
+
"""
|
|
98
|
+
Rotates the given vector with the pose orientation
|
|
99
|
+
|
|
100
|
+
:param v: the vector to be rotated
|
|
101
|
+
|
|
102
|
+
:return: rotated vector
|
|
103
|
+
"""
|
|
104
|
+
return pyquat.Quaternion(self.q).rotate(v)
|
|
105
|
+
|
|
106
|
+
def __str__(self):
|
|
107
|
+
return repr_data(self)
|