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.
- plexus/pulse/common/__init__.py +6 -0
- plexus/pulse/common/carto/OSMFile.py +259 -0
- plexus/pulse/common/carto/OSMNode.py +25 -0
- plexus/pulse/common/carto/OSMTags.py +101 -0
- plexus/pulse/common/carto/OSMWay.py +24 -0
- plexus/pulse/common/carto/__init__.py +11 -0
- plexus/pulse/common/resources/__init__.py +0 -0
- plexus/pulse/common/resources/tags/__init__.py +35 -0
- plexus/pulse/common/resources/tags/unittest-1.0.0.tagset.yaml +98 -0
- plexus/pulse/common/resources/tags/universal-1.0.0.tagset.yaml +1390 -0
- plexus/pulse/common/utils/__init__.py +0 -0
- plexus/pulse/common/utils/apiutils.py +31 -0
- plexus/pulse/common/utils/bagutils.py +331 -0
- plexus/pulse/common/utils/config.py +63 -0
- plexus/pulse/common/utils/datautils.py +233 -0
- plexus/pulse/common/utils/dockerutils.py +181 -0
- plexus/pulse/common/utils/gisutils.py +406 -0
- plexus/pulse/common/utils/jsonutils.py +96 -0
- plexus/pulse/common/utils/ormutils.py +1638 -0
- plexus/pulse/common/utils/pathutils.py +234 -0
- plexus/pulse/common/utils/s3utils.py +939 -0
- plexus/pulse/common/utils/sqlutils.py +9 -0
- plexus/pulse/common/utils/strutils.py +401 -0
- plexus/pulse/common/utils/tagutils.py +1476 -0
- plexus/pulse/common/utils/testutils.py +172 -0
- plexus_python_pulse_common-1.1.75.dist-info/METADATA +42 -0
- plexus_python_pulse_common-1.1.75.dist-info/RECORD +29 -0
- plexus_python_pulse_common-1.1.75.dist-info/WHEEL +5 -0
- plexus_python_pulse_common-1.1.75.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import dataclasses
|
|
3
|
+
import re
|
|
4
|
+
from collections.abc import Generator, Iterator
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import docker
|
|
8
|
+
import docker.errors
|
|
9
|
+
import docker.models.containers
|
|
10
|
+
import docker.models.images
|
|
11
|
+
from plexus.common.utils import logger
|
|
12
|
+
from plexus.common.utils.jsonutils import JsonObject
|
|
13
|
+
from plexus.common.utils.strutils import parse_int_or, trim_to_empty
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ImageName",
|
|
17
|
+
"docker_create_client",
|
|
18
|
+
"docker_build_image",
|
|
19
|
+
"docker_get_image",
|
|
20
|
+
"docker_pull_image",
|
|
21
|
+
"docker_fetch_image",
|
|
22
|
+
"docker_run_detached",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
docker_image_tag_regexp: re.Pattern[str] = re.compile(
|
|
26
|
+
r"^(?P<components>[a-z0-9]+((__?|-+)[a-z0-9]+)*(/[a-z0-9]+((__?|-+)[a-z0-9]+)*)*)(:(?P<tag>\w[\w.-]{0,127}))?$")
|
|
27
|
+
|
|
28
|
+
docker_image_tag_with_registry_regexp = re.compile(
|
|
29
|
+
r"^((?P<host>[a-zA-Z0-9.-]+)(:(?P<port>\d+))?/)?(?P<components>[a-z0-9]+((__?|-+)[a-z0-9]+)*(/[a-z0-9]+((__?|-+)[a-z0-9]+)*)*)(:(?P<tag>\w[\w.-]{0,127}))?$")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclasses.dataclass
|
|
33
|
+
class ImageName(object):
|
|
34
|
+
registry_host: str | None
|
|
35
|
+
registry_port: int | None
|
|
36
|
+
components: list[str]
|
|
37
|
+
tag: str | None
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def registry(self) -> str:
|
|
41
|
+
if self.registry_host is None and self.registry_port is None:
|
|
42
|
+
return ""
|
|
43
|
+
if self.registry_port is None:
|
|
44
|
+
return self.registry_host
|
|
45
|
+
return f"{self.registry_host}:{self.registry_port}"
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def repository(self) -> str:
|
|
49
|
+
return "/".join(self.components)
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def parse(s: str):
|
|
53
|
+
match = docker_image_tag_regexp.match(s)
|
|
54
|
+
if match:
|
|
55
|
+
return ImageName(registry_host=None,
|
|
56
|
+
registry_port=None,
|
|
57
|
+
components=trim_to_empty(match.group("components")).split("/"),
|
|
58
|
+
tag=match.group("tag"))
|
|
59
|
+
|
|
60
|
+
match = docker_image_tag_with_registry_regexp.match(s)
|
|
61
|
+
if match:
|
|
62
|
+
return ImageName(registry_host=match.group("host"),
|
|
63
|
+
registry_port=parse_int_or(match.group("port"), None),
|
|
64
|
+
components=trim_to_empty(match.group("components")).split("/"),
|
|
65
|
+
tag=match.group("tag"))
|
|
66
|
+
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def docker_create_client(
|
|
71
|
+
registry: str,
|
|
72
|
+
username: str,
|
|
73
|
+
password: str,
|
|
74
|
+
) -> contextlib.AbstractContextManager[docker.DockerClient]:
|
|
75
|
+
client = docker.DockerClient()
|
|
76
|
+
client.login(registry=registry, username=username, password=password, reauth=True)
|
|
77
|
+
return contextlib.closing(client)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def docker_build_image(
|
|
81
|
+
client: docker.DockerClient,
|
|
82
|
+
tag: str,
|
|
83
|
+
path: str,
|
|
84
|
+
dockerfile: str,
|
|
85
|
+
build_args: dict[str, str],
|
|
86
|
+
) -> tuple[docker.models.images.Image, Iterator[dict[str, str]]]:
|
|
87
|
+
return client.images.build(tag=tag,
|
|
88
|
+
path=path,
|
|
89
|
+
dockerfile=dockerfile,
|
|
90
|
+
buildargs=build_args,
|
|
91
|
+
rm=True,
|
|
92
|
+
forcerm=True,
|
|
93
|
+
nocache=True)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def docker_get_image(
|
|
97
|
+
client: docker.DockerClient,
|
|
98
|
+
image: str,
|
|
99
|
+
) -> docker.models.images.Image:
|
|
100
|
+
return client.images.get(image)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def docker_pull_image(
|
|
104
|
+
client: docker.DockerClient,
|
|
105
|
+
image: str,
|
|
106
|
+
fallback_local: bool = False,
|
|
107
|
+
) -> docker.models.images.Image:
|
|
108
|
+
try:
|
|
109
|
+
return client.images.pull(image)
|
|
110
|
+
except docker.errors.ImageNotFound:
|
|
111
|
+
if not fallback_local:
|
|
112
|
+
raise
|
|
113
|
+
logger.warning("Image '%s' is not found from remote repository, try local repository instead", image)
|
|
114
|
+
except docker.errors.APIError:
|
|
115
|
+
if not fallback_local:
|
|
116
|
+
raise
|
|
117
|
+
logger.warning("Docker server returns an error while pulling image '%s', try local repository instead", image)
|
|
118
|
+
|
|
119
|
+
return docker_get_image(client, image)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def docker_fetch_image(
|
|
123
|
+
client: docker.DockerClient,
|
|
124
|
+
image: str,
|
|
125
|
+
force_pull: bool = False,
|
|
126
|
+
) -> docker.models.images.Image:
|
|
127
|
+
if force_pull:
|
|
128
|
+
return docker_pull_image(client, image, fallback_local=True)
|
|
129
|
+
try:
|
|
130
|
+
return docker_get_image(client, image)
|
|
131
|
+
except Exception:
|
|
132
|
+
return docker_pull_image(client, image, fallback_local=False)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def docker_run_detached(
|
|
136
|
+
client: docker.DockerClient,
|
|
137
|
+
image: str,
|
|
138
|
+
name: str,
|
|
139
|
+
command: str | list[str],
|
|
140
|
+
volumes: dict[str, dict[str, str]],
|
|
141
|
+
environment: dict[str, str],
|
|
142
|
+
extra_hosts: dict[str, str],
|
|
143
|
+
timeout: int,
|
|
144
|
+
**kwargs,
|
|
145
|
+
) -> tuple[JsonObject, Any]:
|
|
146
|
+
@contextlib.contextmanager
|
|
147
|
+
def managed_docker_run(
|
|
148
|
+
client: docker.DockerClient,
|
|
149
|
+
**kwargs,
|
|
150
|
+
) -> Generator[docker.models.containers.Container, None, None]:
|
|
151
|
+
container_model = None
|
|
152
|
+
try:
|
|
153
|
+
container_model = client.containers.run(**kwargs)
|
|
154
|
+
yield container_model
|
|
155
|
+
finally:
|
|
156
|
+
if container_model is not None:
|
|
157
|
+
if container_model.status != "exited":
|
|
158
|
+
try:
|
|
159
|
+
container_model.stop()
|
|
160
|
+
except docker.errors.DockerException:
|
|
161
|
+
pass
|
|
162
|
+
container_model.wait()
|
|
163
|
+
try:
|
|
164
|
+
container_model.remove()
|
|
165
|
+
except docker.errors.DockerException:
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
run_args = kwargs.copy()
|
|
169
|
+
run_args.update(dict(image=image,
|
|
170
|
+
name=name,
|
|
171
|
+
command=command,
|
|
172
|
+
volumes=volumes,
|
|
173
|
+
environment=environment,
|
|
174
|
+
extra_hosts=extra_hosts,
|
|
175
|
+
detach=True))
|
|
176
|
+
|
|
177
|
+
with managed_docker_run(client, **run_args) as container_model:
|
|
178
|
+
result = container_model.wait(timeout=timeout)
|
|
179
|
+
logs = container_model.logs()
|
|
180
|
+
|
|
181
|
+
return result, logs
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from abc import abstractmethod
|
|
3
|
+
from typing import Annotated, Self
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pydantic as pdt
|
|
7
|
+
import pyproj
|
|
8
|
+
import pyquaternion as pyquat
|
|
9
|
+
from plexus.common.utils.funcutils import singleton
|
|
10
|
+
from plexus.common.utils.strutils import parse_params_string, repr_data, str_conv
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Pose(object):
|
|
14
|
+
@classmethod
|
|
15
|
+
def from_numbers(
|
|
16
|
+
cls,
|
|
17
|
+
px: float,
|
|
18
|
+
py: float,
|
|
19
|
+
pz: float,
|
|
20
|
+
qx: float,
|
|
21
|
+
qy: float,
|
|
22
|
+
qz: float,
|
|
23
|
+
qw: float,
|
|
24
|
+
ts: float = 0,
|
|
25
|
+
) -> Self:
|
|
26
|
+
"""
|
|
27
|
+
Constructs a pose from numbers representing position and orientation
|
|
28
|
+
"""
|
|
29
|
+
return Pose(ts, np.array([px, py, pz], dtype=np.float64), np.array([qw, qx, qy, qz], dtype=np.float64))
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def add(cls, x: Self, d: Self) -> Self:
|
|
33
|
+
"""
|
|
34
|
+
Performs pose SE3 addition, as x + d = y
|
|
35
|
+
"""
|
|
36
|
+
xq = pyquat.Quaternion(x.q)
|
|
37
|
+
dq = pyquat.Quaternion(d.q)
|
|
38
|
+
|
|
39
|
+
yp = x.p + xq.rotate(d.p)
|
|
40
|
+
yq = xq * dq
|
|
41
|
+
|
|
42
|
+
return Pose(0, yp, yq.normalised.elements)
|
|
43
|
+
|
|
44
|
+
@classmethod
|
|
45
|
+
def sub(cls, y: Self, x: Self) -> Self:
|
|
46
|
+
"""
|
|
47
|
+
Performs pose SE3 subtraction, as x + d = y => d = y - x
|
|
48
|
+
"""
|
|
49
|
+
xq = pyquat.Quaternion(x.q)
|
|
50
|
+
yq = pyquat.Quaternion(y.q)
|
|
51
|
+
|
|
52
|
+
dp = xq.inverse.rotate(y.p - x.p)
|
|
53
|
+
dq = xq.inverse * yq
|
|
54
|
+
|
|
55
|
+
return Pose(0, dp, dq.normalised.elements)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def interpolate(cls, a: Self, b: Self, t: float) -> Self:
|
|
59
|
+
"""
|
|
60
|
+
Interpolates between two given poses, as a * t + b * (1 - t)
|
|
61
|
+
|
|
62
|
+
:return: interpolated pose
|
|
63
|
+
"""
|
|
64
|
+
ts = a.ts + (b.ts - a.ts) * t
|
|
65
|
+
p = a.p + (b.p - a.p) * t
|
|
66
|
+
q = pyquat.Quaternion.slerp(pyquat.Quaternion(a.q), pyquat.Quaternion(b.q), t)
|
|
67
|
+
return Pose(ts, p, q.normalised.elements)
|
|
68
|
+
|
|
69
|
+
def __init__(self, ts: float, p: np.ndarray, q: np.ndarray):
|
|
70
|
+
"""
|
|
71
|
+
Represents a pose
|
|
72
|
+
|
|
73
|
+
:param ts: timestamp
|
|
74
|
+
:param p: position vector
|
|
75
|
+
:param q: orientation quaternion
|
|
76
|
+
"""
|
|
77
|
+
self.ts = ts
|
|
78
|
+
self.p = p
|
|
79
|
+
self.q = q
|
|
80
|
+
|
|
81
|
+
def matrix(self) -> np.ndarray:
|
|
82
|
+
"""
|
|
83
|
+
Returns the transformation matrix of this pose
|
|
84
|
+
|
|
85
|
+
:return: transformation matrix
|
|
86
|
+
"""
|
|
87
|
+
r = pyquat.Quaternion(self.q).rotation_matrix
|
|
88
|
+
t = self.p[:, None]
|
|
89
|
+
return np.block([[r, t], [np.zeros((1, 3), dtype=np.float64), np.ones((1, 1), dtype=np.float64)]])
|
|
90
|
+
|
|
91
|
+
def translate(self, v: np.ndarray) -> np.ndarray:
|
|
92
|
+
"""
|
|
93
|
+
Translate the given vector with the pose position
|
|
94
|
+
|
|
95
|
+
:param v: the vector to be translated
|
|
96
|
+
|
|
97
|
+
:return: translated vector
|
|
98
|
+
"""
|
|
99
|
+
return self.p + v
|
|
100
|
+
|
|
101
|
+
def rotate(self, v: np.ndarray) -> np.ndarray:
|
|
102
|
+
"""
|
|
103
|
+
Rotates the given vector with the pose orientation
|
|
104
|
+
|
|
105
|
+
:param v: the vector to be rotated
|
|
106
|
+
|
|
107
|
+
:return: rotated vector
|
|
108
|
+
"""
|
|
109
|
+
return pyquat.Quaternion(self.q).rotate(v)
|
|
110
|
+
|
|
111
|
+
def __str__(self):
|
|
112
|
+
return repr_data(self)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Proj(ABC):
|
|
116
|
+
@classmethod
|
|
117
|
+
@abstractmethod
|
|
118
|
+
def typename(cls) -> str:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
def __init__(self, spec):
|
|
122
|
+
self.spec = spec
|
|
123
|
+
|
|
124
|
+
def __str__(self):
|
|
125
|
+
return self.to_string()
|
|
126
|
+
|
|
127
|
+
def method(self) -> str:
|
|
128
|
+
return self.typename()
|
|
129
|
+
|
|
130
|
+
@abstractmethod
|
|
131
|
+
def ellipsoid(self) -> str:
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
@abstractmethod
|
|
135
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
136
|
+
"""
|
|
137
|
+
Performs forward projection from latitude/longitude to projection coordinate system
|
|
138
|
+
|
|
139
|
+
:param lat: latitude
|
|
140
|
+
:param lon: longitude
|
|
141
|
+
|
|
142
|
+
:return: tuple of x and y in projection coordinate system
|
|
143
|
+
"""
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
@abstractmethod
|
|
147
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
148
|
+
"""
|
|
149
|
+
Performs inverse projection from projection coordinate system to latitude/longitude
|
|
150
|
+
|
|
151
|
+
:param x: x in projection coordinate system
|
|
152
|
+
:param y: y in projection coordinate system
|
|
153
|
+
|
|
154
|
+
:return: tuple of latitude and longitude
|
|
155
|
+
"""
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
@abstractmethod
|
|
159
|
+
def to_string(self) -> str:
|
|
160
|
+
pass
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class EQDCProj(Proj):
|
|
164
|
+
"""
|
|
165
|
+
Equidistant conic projection
|
|
166
|
+
Please refer to https://en.wikipedia.org/wiki/Equidistant_conic_projection
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
class Spec(pdt.BaseModel):
|
|
170
|
+
lat_1: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)]
|
|
171
|
+
lat_2: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)]
|
|
172
|
+
lon_0: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)] = 0.0
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def typename(cls) -> str:
|
|
176
|
+
return "eqdc"
|
|
177
|
+
|
|
178
|
+
def __init__(self, **kwargs):
|
|
179
|
+
super(EQDCProj, self).__init__(EQDCProj.Spec(**kwargs))
|
|
180
|
+
self.proj = self.make_proj()
|
|
181
|
+
|
|
182
|
+
def ellipsoid(self) -> str:
|
|
183
|
+
return "WGS84"
|
|
184
|
+
|
|
185
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
186
|
+
x, y = self.proj(lon, lat)
|
|
187
|
+
return x, y
|
|
188
|
+
|
|
189
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
190
|
+
lon, lat = self.proj(x, y, inverse=True)
|
|
191
|
+
return lat, lon
|
|
192
|
+
|
|
193
|
+
def to_string(self) -> str:
|
|
194
|
+
return "{method}:lat_1={},lat_2={},lon_0={}".format(
|
|
195
|
+
self.spec.lat_1,
|
|
196
|
+
self.spec.lat_2,
|
|
197
|
+
self.spec.lon_0,
|
|
198
|
+
method=self.method(),
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def make_proj(self):
|
|
202
|
+
return pyproj.Proj(
|
|
203
|
+
"+proj={method} +ellps={ellipsoid} +datum={ellipsoid} +units=m +no_defs +lat_1={} +lat_2={} +lon_0={}".format(
|
|
204
|
+
self.spec.lat_1,
|
|
205
|
+
self.spec.lat_2,
|
|
206
|
+
self.spec.lon_0,
|
|
207
|
+
method=self.method(),
|
|
208
|
+
ellipsoid=self.ellipsoid(),
|
|
209
|
+
)
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class UTMProj(Proj):
|
|
214
|
+
"""
|
|
215
|
+
Universal Transverse Mercator coordinate system
|
|
216
|
+
Please refer to https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
class Spec(pdt.BaseModel):
|
|
220
|
+
zone: Annotated[int, pdt.Strict()]
|
|
221
|
+
south: Annotated[bool, pdt.Strict()] = False
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def typename(cls) -> str:
|
|
225
|
+
return "utm"
|
|
226
|
+
|
|
227
|
+
def __init__(self, **kwargs):
|
|
228
|
+
super(UTMProj, self).__init__(UTMProj.Spec(**kwargs))
|
|
229
|
+
self.proj = self.make_proj()
|
|
230
|
+
|
|
231
|
+
def ellipsoid(self) -> str:
|
|
232
|
+
return "WGS84"
|
|
233
|
+
|
|
234
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
235
|
+
x, y = self.proj(lon, lat)
|
|
236
|
+
return x, y
|
|
237
|
+
|
|
238
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
239
|
+
lon, lat = self.proj(x, y, inverse=True)
|
|
240
|
+
return lat, lon
|
|
241
|
+
|
|
242
|
+
def to_string(self) -> str:
|
|
243
|
+
return "{method}:zone={},south={}".format(self.spec.zone, self.spec.south, method=self.method())
|
|
244
|
+
|
|
245
|
+
def make_proj(self):
|
|
246
|
+
return pyproj.Proj(
|
|
247
|
+
"+proj={method} +ellps={ellipsoid} +datum={ellipsoid} +units=m +no_defs +zone={} {}".format(
|
|
248
|
+
self.spec.zone,
|
|
249
|
+
"+south" if self.spec.south else "",
|
|
250
|
+
method=self.method(),
|
|
251
|
+
ellipsoid=self.ellipsoid(),
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
class WebMercProj(Proj):
|
|
257
|
+
"""
|
|
258
|
+
Web Mercator projection
|
|
259
|
+
Please refer to https://en.wikipedia.org/wiki/Web_Mercator_projection
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
class Spec(pdt.BaseModel):
|
|
263
|
+
lon_0: Annotated[float, pdt.Strict(), pdt.AllowInfNan(False)] = 0.0
|
|
264
|
+
|
|
265
|
+
@classmethod
|
|
266
|
+
def typename(cls) -> str:
|
|
267
|
+
return "webmerc"
|
|
268
|
+
|
|
269
|
+
def __init__(self, **kwargs):
|
|
270
|
+
super(WebMercProj, self).__init__(WebMercProj.Spec(**kwargs))
|
|
271
|
+
self.proj = self.make_proj()
|
|
272
|
+
|
|
273
|
+
def ellipsoid(self) -> str:
|
|
274
|
+
return "WGS84"
|
|
275
|
+
|
|
276
|
+
def from_latlon(self, lat: float, lon: float) -> tuple[float, float]:
|
|
277
|
+
x, y = self.proj(lon, lat)
|
|
278
|
+
return x, y
|
|
279
|
+
|
|
280
|
+
def to_latlon(self, x: float, y: float) -> tuple[float, float]:
|
|
281
|
+
lon, lat = self.proj(x, y, inverse=True)
|
|
282
|
+
return lat, lon
|
|
283
|
+
|
|
284
|
+
def to_string(self) -> str:
|
|
285
|
+
return "{method}:lon_0={}".format(self.spec.lon_0, method=self.method())
|
|
286
|
+
|
|
287
|
+
def make_proj(self):
|
|
288
|
+
return pyproj.Proj(
|
|
289
|
+
"+proj={method} +ellps={ellipsoid} +datum={ellipsoid} +units=m +no_defs +lon_0={}".format(
|
|
290
|
+
self.spec.lon_0,
|
|
291
|
+
method=self.method(),
|
|
292
|
+
ellipsoid=self.ellipsoid(),
|
|
293
|
+
),
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
@singleton
|
|
298
|
+
def default_proj() -> Proj:
|
|
299
|
+
return make_proj("eqdc:lat_1=30.0,lat_2=50.0")
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def make_proj(spec_str: str) -> Proj | None:
|
|
303
|
+
"""
|
|
304
|
+
Constructs cartographic projection from the given name and arguments
|
|
305
|
+
|
|
306
|
+
:param spec_str: projection spec string
|
|
307
|
+
|
|
308
|
+
:return: projection
|
|
309
|
+
"""
|
|
310
|
+
|
|
311
|
+
if spec_str is None:
|
|
312
|
+
return None
|
|
313
|
+
|
|
314
|
+
match spec_str.split(":", maxsplit=1):
|
|
315
|
+
case [name]:
|
|
316
|
+
kwargs = {}
|
|
317
|
+
case [name, args]:
|
|
318
|
+
kwargs = parse_params_string(args, str_parser=str_conv)
|
|
319
|
+
case _:
|
|
320
|
+
raise ValueError("bad spec string")
|
|
321
|
+
|
|
322
|
+
if name.lower() == UTMProj.typename():
|
|
323
|
+
return UTMProj(**kwargs)
|
|
324
|
+
if name.lower() == EQDCProj.typename():
|
|
325
|
+
return EQDCProj(**kwargs)
|
|
326
|
+
if name.lower() == WebMercProj.typename():
|
|
327
|
+
return WebMercProj(**kwargs)
|
|
328
|
+
|
|
329
|
+
return None
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
class Coord(object):
|
|
333
|
+
@classmethod
|
|
334
|
+
def from_pose(cls, pose: Pose, proj: Proj | None) -> Self:
|
|
335
|
+
"""
|
|
336
|
+
Creates a coord from given pose
|
|
337
|
+
|
|
338
|
+
:param pose: pose based on which the coord is created
|
|
339
|
+
:param proj: cartographic projection
|
|
340
|
+
|
|
341
|
+
:return: generated coord
|
|
342
|
+
"""
|
|
343
|
+
return cls.from_xy(pose.p[0], pose.p[1], pose.p[2], proj, pose.ts)
|
|
344
|
+
|
|
345
|
+
@classmethod
|
|
346
|
+
def from_latlon(cls, lat: float, lon: float, ele: float, proj: Proj | None, ts: float = 0) -> Self:
|
|
347
|
+
"""
|
|
348
|
+
Creates a coord from latitude/longitude
|
|
349
|
+
|
|
350
|
+
:param lat: latitude
|
|
351
|
+
:param lon: longitude
|
|
352
|
+
:param ele: elevation
|
|
353
|
+
:param proj: cartographic projection
|
|
354
|
+
:param ts: timestamp
|
|
355
|
+
|
|
356
|
+
:return: generated coord
|
|
357
|
+
"""
|
|
358
|
+
if proj is None:
|
|
359
|
+
return Coord(ts, lat, lon, ele, 0.0, 0.0, pdt.BaseModel())
|
|
360
|
+
|
|
361
|
+
x, y = proj.from_latlon(lat, lon)
|
|
362
|
+
return Coord(ts, lat, lon, ele, x, y, proj.spec)
|
|
363
|
+
|
|
364
|
+
@classmethod
|
|
365
|
+
def from_xy(cls, x: float, y: float, ele: float, proj: Proj | None, ts: float = 0) -> Self:
|
|
366
|
+
"""
|
|
367
|
+
Creates a coord from coordinate in cartographic projection
|
|
368
|
+
|
|
369
|
+
:param x: x coordinate in cartographic projection
|
|
370
|
+
:param y: y coordinate in cartographic projection
|
|
371
|
+
:param ele: elevation
|
|
372
|
+
:param proj: cartographic projection
|
|
373
|
+
:param ts: timestamp
|
|
374
|
+
|
|
375
|
+
:return: a coord
|
|
376
|
+
"""
|
|
377
|
+
if proj is None:
|
|
378
|
+
return Coord(ts, 0.0, 0.0, ele, x, y, pdt.BaseModel())
|
|
379
|
+
|
|
380
|
+
lat, lon = proj.to_latlon(x, y)
|
|
381
|
+
return Coord(ts, lat, lon, ele, x, y, proj.spec)
|
|
382
|
+
|
|
383
|
+
def __init__(self, ts: float, lat: float, lon: float, ele: float, x: float, y: float, spec: pdt.BaseModel):
|
|
384
|
+
"""
|
|
385
|
+
Represents a coordinate
|
|
386
|
+
|
|
387
|
+
:param ts: timestamp
|
|
388
|
+
:param lat: latitude
|
|
389
|
+
:param lon: longitude
|
|
390
|
+
:param ele: elevation
|
|
391
|
+
:param x: x in projection coordinate system
|
|
392
|
+
:param y: y in projection coordinate system
|
|
393
|
+
:param spec: projection specification
|
|
394
|
+
"""
|
|
395
|
+
self.ts = ts
|
|
396
|
+
self.lat = lat
|
|
397
|
+
self.lon = lon
|
|
398
|
+
self.x = x
|
|
399
|
+
self.y = y
|
|
400
|
+
self.spec = spec
|
|
401
|
+
self.ele = ele
|
|
402
|
+
self.xy = np.array([x, y], dtype=np.float64)
|
|
403
|
+
self.xyz = np.array([x, y, ele], dtype=np.float64)
|
|
404
|
+
|
|
405
|
+
def __str__(self):
|
|
406
|
+
return repr_data(self)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import os
|
|
3
|
+
from collections.abc import Generator, Iterable
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import ujson as json
|
|
7
|
+
from plexus.common.utils.dtutils import dt_format, dt_parse, extended_format
|
|
8
|
+
from plexus.common.utils.iterutils import batched
|
|
9
|
+
from plexus.common.utils.jsonutils import JsonType, JsonValueCompatible
|
|
10
|
+
from plexus.common.utils.jsonutils import json_reformat
|
|
11
|
+
|
|
12
|
+
from plexus.pulse.common.utils.pathutils import collect_volumed_filenames, populate_volumed_filenames
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"json_datetime_decoder",
|
|
16
|
+
"json_datetime_encoder",
|
|
17
|
+
"json_loads",
|
|
18
|
+
"json_dumps",
|
|
19
|
+
"read_chunked_jsonl",
|
|
20
|
+
"write_chunked_jsonl",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def json_datetime_decoder(v: Any) -> datetime.datetime | None:
|
|
25
|
+
if v is None:
|
|
26
|
+
return None
|
|
27
|
+
if isinstance(v, str):
|
|
28
|
+
return json_datetime_decoder(dt_parse(v, extended_format(with_us=True, with_tz=True)))
|
|
29
|
+
if isinstance(v, datetime.datetime):
|
|
30
|
+
return v.replace(tzinfo=datetime.timezone.utc)
|
|
31
|
+
raise ValueError("unexpected type of value for datetime decoder")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def json_datetime_encoder(v: Any) -> str | None:
|
|
35
|
+
if v is None:
|
|
36
|
+
return None
|
|
37
|
+
if isinstance(v, str):
|
|
38
|
+
return json_datetime_encoder(dt_parse(v, extended_format(with_us=True, with_tz=True)))
|
|
39
|
+
if isinstance(v, datetime.datetime):
|
|
40
|
+
return dt_format(v.replace(tzinfo=datetime.timezone.utc), extended_format(with_us=True, with_tz=True))
|
|
41
|
+
raise ValueError("unexpected type of value for datetime encoder")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def json_deserializer(obj):
|
|
45
|
+
def value_formatter(value: JsonValueCompatible) -> JsonType:
|
|
46
|
+
if not isinstance(value, str):
|
|
47
|
+
return value
|
|
48
|
+
try:
|
|
49
|
+
return dt_parse(value, extended_format(with_us=True, with_tz=True))
|
|
50
|
+
except Exception:
|
|
51
|
+
return value
|
|
52
|
+
|
|
53
|
+
return json_reformat(obj, value_formatter=value_formatter)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def json_serializer(obj):
|
|
57
|
+
def unregistered_formatter(unregistered: Any) -> JsonType:
|
|
58
|
+
if isinstance(unregistered, datetime.datetime):
|
|
59
|
+
return dt_format(unregistered, extended_format(with_us=True, with_tz=True))
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
return json_reformat(obj, raise_if_unregistered=False, unregistered_formatter=unregistered_formatter)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def json_loads(s: str) -> JsonType:
|
|
66
|
+
return json_deserializer(json.loads(s))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def json_dumps(obj: JsonType) -> str:
|
|
70
|
+
return json.dumps(json_serializer(obj), ensure_ascii=False, escape_forward_slashes=False)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def read_chunked_jsonl(template: str) -> Generator[tuple[JsonType, str], None, None]:
|
|
74
|
+
for path, _ in collect_volumed_filenames(template):
|
|
75
|
+
with open(path, mode="r", encoding="utf-8") as fh:
|
|
76
|
+
for line in fh:
|
|
77
|
+
yield json_loads(line), path
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def write_chunked_jsonl(records: Iterable[JsonType], template: str, chunk_size: int) -> list[tuple[str, int]]:
|
|
81
|
+
generator = populate_volumed_filenames(template)
|
|
82
|
+
entry = []
|
|
83
|
+
for batch_index, batch in enumerate(batched(records, chunk_size)):
|
|
84
|
+
path, _ = next(generator)
|
|
85
|
+
lines = 0
|
|
86
|
+
with open(path, mode="w") as fh:
|
|
87
|
+
for record in batch:
|
|
88
|
+
fh.write(json_dumps(record))
|
|
89
|
+
fh.write("\n")
|
|
90
|
+
lines += 1
|
|
91
|
+
entry.append((path, lines))
|
|
92
|
+
if len(entry) == 1:
|
|
93
|
+
path, lines = entry[0]
|
|
94
|
+
os.rename(path, template)
|
|
95
|
+
return [(template, lines)]
|
|
96
|
+
return entry
|