compass-lib 0.0.2__py3-none-any.whl → 0.0.3__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.
- compass_lib/__init__.py +113 -3
- compass_lib/commands/__init__.py +2 -0
- compass_lib/commands/convert.py +223 -36
- compass_lib/commands/encrypt.py +33 -7
- compass_lib/commands/geojson.py +118 -0
- compass_lib/commands/main.py +1 -1
- compass_lib/constants.py +84 -36
- compass_lib/enums.py +292 -84
- compass_lib/errors.py +86 -0
- compass_lib/geo_utils.py +47 -0
- compass_lib/geojson.py +1024 -0
- compass_lib/interface.py +332 -0
- compass_lib/io.py +246 -0
- compass_lib/models.py +217 -95
- compass_lib/plot/__init__.py +28 -0
- compass_lib/plot/models.py +265 -0
- compass_lib/plot/parser.py +610 -0
- compass_lib/project/__init__.py +36 -0
- compass_lib/project/format.py +158 -0
- compass_lib/project/models.py +494 -0
- compass_lib/project/parser.py +638 -0
- compass_lib/survey/__init__.py +24 -0
- compass_lib/survey/format.py +284 -0
- compass_lib/survey/models.py +160 -0
- compass_lib/survey/parser.py +842 -0
- compass_lib/validation.py +74 -0
- {compass_lib-0.0.2.dist-info → compass_lib-0.0.3.dist-info}/METADATA +8 -11
- compass_lib-0.0.3.dist-info/RECORD +31 -0
- {compass_lib-0.0.2.dist-info → compass_lib-0.0.3.dist-info}/entry_points.txt +2 -1
- compass_lib/encoding.py +0 -27
- compass_lib/parser.py +0 -435
- compass_lib/utils.py +0 -15
- compass_lib-0.0.2.dist-info/RECORD +0 -16
- {compass_lib-0.0.2.dist-info → compass_lib-0.0.3.dist-info}/WHEEL +0 -0
- {compass_lib-0.0.2.dist-info → compass_lib-0.0.3.dist-info}/licenses/LICENSE +0 -0
compass_lib/models.py
CHANGED
|
@@ -1,129 +1,251 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Core data models for Compass file formats.
|
|
3
|
+
|
|
4
|
+
This module contains the base Pydantic models used across
|
|
5
|
+
survey, project, and plot parsing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from __future__ import annotations
|
|
2
9
|
|
|
3
|
-
import datetime # noqa: TC003
|
|
4
|
-
import json
|
|
5
|
-
from pathlib import Path
|
|
6
10
|
from typing import Annotated
|
|
7
|
-
from typing import Any
|
|
8
11
|
|
|
9
12
|
from pydantic import BaseModel
|
|
10
|
-
from pydantic import ConfigDict
|
|
11
13
|
from pydantic import Field
|
|
12
14
|
from pydantic import field_validator
|
|
15
|
+
from pyproj import CRS
|
|
16
|
+
from pyproj import Transformer
|
|
17
|
+
|
|
18
|
+
from compass_lib.enums import Datum
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class NEVLocation(BaseModel):
|
|
22
|
+
"""3D location with Northing, Easting, and Vertical (elevation) components.
|
|
23
|
+
|
|
24
|
+
All values are stored with their associated unit. The unit for all three
|
|
25
|
+
components must be the same.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
easting: East-West coordinate
|
|
29
|
+
northing: North-South coordinate
|
|
30
|
+
elevation: Vertical elevation
|
|
31
|
+
unit: The length unit for all coordinates ('f' for feet, 'm' for meters)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
easting: float
|
|
35
|
+
northing: float
|
|
36
|
+
elevation: float
|
|
37
|
+
unit: Annotated[str, Field(default="f", pattern="^[fmFM]$")]
|
|
38
|
+
|
|
39
|
+
def __str__(self) -> str:
|
|
40
|
+
"""Format as human-readable string."""
|
|
41
|
+
return (
|
|
42
|
+
f"NEVLocation(easting={self.easting}, "
|
|
43
|
+
f"northing={self.northing}, "
|
|
44
|
+
f"elevation={self.elevation}, "
|
|
45
|
+
f"unit={self.unit})"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class UTMLocation(BaseModel):
|
|
50
|
+
"""Represents a UTM-based coordinate for fixed stations."""
|
|
51
|
+
|
|
52
|
+
easting: Annotated[
|
|
53
|
+
float,
|
|
54
|
+
Field(
|
|
55
|
+
ge=166_000,
|
|
56
|
+
le=834_000,
|
|
57
|
+
description="Easting coordinate in meters (valid UTM range)",
|
|
58
|
+
),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
northing: Annotated[
|
|
62
|
+
float,
|
|
63
|
+
Field(ge=0, le=10_000_000, description="Northing coordinate in meters"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
elevation: Annotated[
|
|
67
|
+
float,
|
|
68
|
+
Field(
|
|
69
|
+
ge=-435.0, # Dead Sea (Israel/Jordan/West Bank)
|
|
70
|
+
le=8850, # Everest
|
|
71
|
+
description="Elevation in meters",
|
|
72
|
+
),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
zone: Annotated[
|
|
76
|
+
int,
|
|
77
|
+
Field(
|
|
78
|
+
ge=-60,
|
|
79
|
+
le=60,
|
|
80
|
+
description="UTM zone number (1-60 north, -1 to -60 south, 0 not allowed)",
|
|
81
|
+
),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
convergence: Annotated[
|
|
85
|
+
float,
|
|
86
|
+
Field(default=0.0, ge=-5.0, le=5.0, description="Convergence angle in degrees"),
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
datum: Annotated[
|
|
90
|
+
Datum | None,
|
|
91
|
+
Field(
|
|
92
|
+
default=None,
|
|
93
|
+
description="Datum (e.g., Datum.NORTH_AMERICAN_1927, Datum.WGS_1984)",
|
|
94
|
+
),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# -----------------------------
|
|
98
|
+
# Validators
|
|
99
|
+
# -----------------------------
|
|
100
|
+
|
|
101
|
+
@field_validator("zone")
|
|
102
|
+
@classmethod
|
|
103
|
+
def validate_zone(cls, v: int) -> int:
|
|
104
|
+
"""Validate UTM zone number.
|
|
105
|
+
|
|
106
|
+
Positive values (1-60) indicate northern hemisphere.
|
|
107
|
+
Negative values (-1 to -60) indicate southern hemisphere.
|
|
108
|
+
Zero is not allowed.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
v: Zone number
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
Validated zone number
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
ValueError: If zone is 0 or abs(zone) > 60
|
|
118
|
+
"""
|
|
119
|
+
if v == 0:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
"UTM zone cannot be 0. Use 1-60 for north, -1 to -60 for south."
|
|
122
|
+
)
|
|
123
|
+
if abs(v) > 60:
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"UTM zone must be between -60 and 60 (excluding 0), got {v}"
|
|
126
|
+
)
|
|
127
|
+
return v
|
|
128
|
+
|
|
129
|
+
@field_validator("datum", mode="before")
|
|
130
|
+
@classmethod
|
|
131
|
+
def normalize_datum(cls, value: str | Datum | None) -> Datum | None:
|
|
132
|
+
"""Validate and normalize datum string to Datum enum.
|
|
13
133
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# from compass_lib.errors import DuplicateValueError
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class SurveyShot(BaseModel):
|
|
20
|
-
from_id: str
|
|
21
|
-
to_id: str
|
|
22
|
-
|
|
23
|
-
azimuth: Annotated[float, Field(ge=0, lt=360)]
|
|
134
|
+
Accepts either a string (which will be normalized) or a Datum enum value.
|
|
24
135
|
|
|
25
|
-
|
|
26
|
-
|
|
136
|
+
Args:
|
|
137
|
+
value: Datum as string or Datum enum, or None
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Datum enum value or None
|
|
27
141
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
142
|
+
Raises:
|
|
143
|
+
ValueError: If datum string is not recognized
|
|
144
|
+
"""
|
|
145
|
+
if value is None or isinstance(value, Datum):
|
|
146
|
+
return value
|
|
31
147
|
|
|
32
|
-
|
|
33
|
-
|
|
148
|
+
# Normalize string to Datum enum
|
|
149
|
+
return Datum.normalize(value)
|
|
34
150
|
|
|
35
|
-
#
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
up: Annotated[float, Field(ge=0)] = 0.0
|
|
39
|
-
down: Annotated[float, Field(ge=0)] = 0.0
|
|
151
|
+
# -----------------------------
|
|
152
|
+
# Properties
|
|
153
|
+
# -----------------------------
|
|
40
154
|
|
|
41
|
-
|
|
155
|
+
@property
|
|
156
|
+
def is_northern_hemisphere(self) -> bool:
|
|
157
|
+
"""Check if this location is in the northern hemisphere.
|
|
42
158
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return
|
|
159
|
+
Returns:
|
|
160
|
+
True if northern hemisphere (zone > 0), False if southern (zone < 0)
|
|
161
|
+
"""
|
|
162
|
+
return self.zone > 0
|
|
47
163
|
|
|
48
|
-
@
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return value if value > 0 else 0.0
|
|
164
|
+
@property
|
|
165
|
+
def zone_number(self) -> int:
|
|
166
|
+
"""Get the absolute zone number (1-60).
|
|
52
167
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return
|
|
168
|
+
Returns:
|
|
169
|
+
Absolute value of the zone number
|
|
170
|
+
"""
|
|
171
|
+
return abs(self.zone)
|
|
57
172
|
|
|
58
|
-
#
|
|
173
|
+
# -----------------------------
|
|
174
|
+
# Methods
|
|
175
|
+
# -----------------------------
|
|
59
176
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
# dupl_vals = list(duplicates(vals2check))
|
|
64
|
-
# if dupl_vals:
|
|
65
|
-
# raise DuplicateValueError(
|
|
66
|
-
# f"[{cls.__name__}] Duplicate value found for `{field}`: "
|
|
67
|
-
# f"{dupl_vals}"
|
|
68
|
-
# )
|
|
69
|
-
# return values
|
|
177
|
+
def to_latlon(self) -> tuple[float, float]:
|
|
178
|
+
"""
|
|
179
|
+
Convert this UTM location to GPS coordinates (latitude, longitude).
|
|
70
180
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
# Not autogenerated ones. Hence we need to register the name."""
|
|
181
|
+
NOTE: This method takes the decision to exclusively use DATUM WGS 1984 for uniformity
|
|
182
|
+
and ignore the datum from the MAK project file.
|
|
183
|
+
This allows for a consistent and predictable conversion of UTM coordinates to GPS coordinates.
|
|
184
|
+
And inter-operability with other software that uses WGS 1984 for GPS coordinates.
|
|
76
185
|
|
|
77
|
-
|
|
78
|
-
|
|
186
|
+
The hemisphere is determined by the sign of the zone:
|
|
187
|
+
- Positive zone (1-60): Northern hemisphere
|
|
188
|
+
- Negative zone (-1 to -60): Southern hemisphere
|
|
79
189
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
# if char.upper() not in [
|
|
83
|
-
# *UniqueNameGenerator.VOCAB,
|
|
84
|
-
# *list("_-~:!?.'()[]{}@*&#%|$")
|
|
85
|
-
# ]:
|
|
86
|
-
# raise ValueError(f"The character `{char}` is not allowed as `name`.")
|
|
190
|
+
Args:
|
|
191
|
+
None
|
|
87
192
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
193
|
+
Returns:
|
|
194
|
+
(lat, lon) in decimal degrees
|
|
195
|
+
""" # noqa: E501
|
|
91
196
|
|
|
92
|
-
|
|
93
|
-
|
|
197
|
+
WGS_1984_EPSG = 4326
|
|
198
|
+
geographic_crs = CRS.from_epsg(WGS_1984_EPSG)
|
|
94
199
|
|
|
200
|
+
# ---- Build UTM CRS ----
|
|
201
|
+
# Note: pyproj expects "WGS84" (no space) in proj4 strings, not "WGS 1984"
|
|
202
|
+
# Note: pyproj requires positive zone number and hemisphere specified separately
|
|
203
|
+
hemisphere = "+north" if self.is_northern_hemisphere else "+south"
|
|
204
|
+
utm_crs = CRS.from_proj4(
|
|
205
|
+
f"+proj=utm +zone={self.zone_number} {hemisphere} +datum=WGS84 +units=m +no_defs" # noqa: E501
|
|
206
|
+
)
|
|
95
207
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
survey_date: datetime.date | None = None
|
|
102
|
-
discovery_date: datetime.date | None = None
|
|
103
|
-
declination: float
|
|
104
|
-
format: str = "DDDDUDLRLADN"
|
|
105
|
-
shots: list[SurveyShot]
|
|
106
|
-
surveyors: str | None = None
|
|
208
|
+
transformer = Transformer.from_crs(
|
|
209
|
+
utm_crs,
|
|
210
|
+
geographic_crs,
|
|
211
|
+
always_xy=True,
|
|
212
|
+
)
|
|
107
213
|
|
|
108
|
-
|
|
214
|
+
lon, lat = transformer.transform(self.easting, self.northing)
|
|
215
|
+
return lat, lon
|
|
109
216
|
|
|
110
217
|
|
|
111
|
-
class
|
|
112
|
-
|
|
113
|
-
description: str = ""
|
|
218
|
+
class Location(BaseModel):
|
|
219
|
+
"""3D plot location with northing, easting, and vertical components.
|
|
114
220
|
|
|
115
|
-
|
|
221
|
+
Used for plot file commands. Values are in feet.
|
|
116
222
|
|
|
117
|
-
|
|
223
|
+
Attributes:
|
|
224
|
+
northing: North-South coordinate (feet)
|
|
225
|
+
easting: East-West coordinate (feet)
|
|
226
|
+
vertical: Vertical coordinate (feet)
|
|
227
|
+
"""
|
|
118
228
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
229
|
+
northing: float | None = None
|
|
230
|
+
easting: float | None = None
|
|
231
|
+
vertical: float | None = None
|
|
232
|
+
|
|
233
|
+
def __str__(self) -> str:
|
|
234
|
+
"""Format as human-readable string."""
|
|
235
|
+
return (
|
|
236
|
+
f"Location(northing={self.northing}, "
|
|
237
|
+
f"easting={self.easting}, "
|
|
238
|
+
f"vertical={self.vertical})"
|
|
239
|
+
)
|
|
122
240
|
|
|
123
|
-
json_str = json.dumps(data, indent=4, sort_keys=True, cls=EnhancedJSONEncoder)
|
|
124
241
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
242
|
+
class Bounds(BaseModel):
|
|
243
|
+
"""Bounding box with lower and upper bounds.
|
|
244
|
+
|
|
245
|
+
Attributes:
|
|
246
|
+
lower: Lower bound location
|
|
247
|
+
upper: Upper bound location
|
|
248
|
+
"""
|
|
128
249
|
|
|
129
|
-
|
|
250
|
+
lower: Location = Field(default_factory=Location)
|
|
251
|
+
upper: Location = Field(default_factory=Location)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Plot module for parsing Compass .PLT files."""
|
|
3
|
+
|
|
4
|
+
from compass_lib.plot.models import BeginFeatureCommand
|
|
5
|
+
from compass_lib.plot.models import BeginSectionCommand
|
|
6
|
+
from compass_lib.plot.models import BeginSurveyCommand
|
|
7
|
+
from compass_lib.plot.models import CaveBoundsCommand
|
|
8
|
+
from compass_lib.plot.models import CompassPlotCommand
|
|
9
|
+
from compass_lib.plot.models import DatumCommand
|
|
10
|
+
from compass_lib.plot.models import DrawSurveyCommand
|
|
11
|
+
from compass_lib.plot.models import FeatureCommand
|
|
12
|
+
from compass_lib.plot.models import SurveyBoundsCommand
|
|
13
|
+
from compass_lib.plot.models import UtmZoneCommand
|
|
14
|
+
from compass_lib.plot.parser import CompassPlotParser
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"BeginFeatureCommand",
|
|
18
|
+
"BeginSectionCommand",
|
|
19
|
+
"BeginSurveyCommand",
|
|
20
|
+
"CaveBoundsCommand",
|
|
21
|
+
"CompassPlotCommand",
|
|
22
|
+
"CompassPlotParser",
|
|
23
|
+
"DatumCommand",
|
|
24
|
+
"DrawSurveyCommand",
|
|
25
|
+
"FeatureCommand",
|
|
26
|
+
"SurveyBoundsCommand",
|
|
27
|
+
"UtmZoneCommand",
|
|
28
|
+
]
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Plot data models for Compass .PLT files.
|
|
3
|
+
|
|
4
|
+
This module contains Pydantic models for representing plot commands
|
|
5
|
+
found in Compass .PLT plot files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel
|
|
12
|
+
from pydantic import Field
|
|
13
|
+
from pydantic import field_validator
|
|
14
|
+
|
|
15
|
+
from compass_lib.enums import Datum
|
|
16
|
+
from compass_lib.enums import DrawOperation
|
|
17
|
+
from compass_lib.models import Bounds
|
|
18
|
+
from compass_lib.models import Location
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CompassPlotCommand(BaseModel):
|
|
22
|
+
"""Base class for all plot commands."""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BeginSurveyCommand(CompassPlotCommand):
|
|
26
|
+
"""Begin survey command (N).
|
|
27
|
+
|
|
28
|
+
Marks the start of a new survey section.
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
survey_name: Survey identifier (max 12 chars)
|
|
32
|
+
date: Survey date
|
|
33
|
+
comment: Optional comment (max 80 chars)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
survey_name: str
|
|
37
|
+
date: datetime.date | None = None
|
|
38
|
+
comment: str | None = None
|
|
39
|
+
|
|
40
|
+
def __str__(self) -> str:
|
|
41
|
+
"""Format as PLT file syntax."""
|
|
42
|
+
result = f"N{self.survey_name[:12]}"
|
|
43
|
+
if self.date:
|
|
44
|
+
result += f"\tD {self.date.month} {self.date.day} {self.date.year}"
|
|
45
|
+
else:
|
|
46
|
+
result += "\tD 1 1 1"
|
|
47
|
+
if self.comment:
|
|
48
|
+
result += f"\tC{self.comment[:80]}"
|
|
49
|
+
return result
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class BeginSectionCommand(CompassPlotCommand):
|
|
53
|
+
"""Begin section command (S).
|
|
54
|
+
|
|
55
|
+
Marks the start of a new section.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
section_name: Section name (max 20 chars)
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
section_name: str
|
|
62
|
+
|
|
63
|
+
def __str__(self) -> str:
|
|
64
|
+
"""Format as PLT file syntax."""
|
|
65
|
+
return f"S{self.section_name[:20]}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class BeginFeatureCommand(CompassPlotCommand):
|
|
69
|
+
"""Begin feature command (F).
|
|
70
|
+
|
|
71
|
+
Marks the start of a feature definition with optional range.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
feature_name: Feature name (max 12 chars)
|
|
75
|
+
min_value: Optional minimum value for range
|
|
76
|
+
max_value: Optional maximum value for range
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
feature_name: str
|
|
80
|
+
min_value: Decimal | None = None
|
|
81
|
+
max_value: Decimal | None = None
|
|
82
|
+
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
"""Format as PLT file syntax."""
|
|
85
|
+
result = f"F{self.feature_name[:12]}"
|
|
86
|
+
if self.min_value is not None and self.max_value is not None:
|
|
87
|
+
result += f"\tR\t{float(self.min_value)}\t{float(self.max_value)}"
|
|
88
|
+
return result
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class DrawSurveyCommand(CompassPlotCommand):
|
|
92
|
+
"""Draw survey command (M or D).
|
|
93
|
+
|
|
94
|
+
Represents a move-to or draw-to operation with station data.
|
|
95
|
+
|
|
96
|
+
Attributes:
|
|
97
|
+
operation: MOVE_TO (M) or LINE_TO (D)
|
|
98
|
+
location: 3D coordinates
|
|
99
|
+
station_name: Station identifier
|
|
100
|
+
left: Distance to left wall (feet, None if missing)
|
|
101
|
+
right: Distance to right wall (feet, None if missing)
|
|
102
|
+
up: Distance to ceiling (feet, None if missing)
|
|
103
|
+
down: Distance to floor (feet, None if missing)
|
|
104
|
+
distance_from_entrance: Distance from cave entrance (feet)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
operation: DrawOperation
|
|
108
|
+
location: Location = Field(default_factory=Location)
|
|
109
|
+
station_name: str | None = None
|
|
110
|
+
left: float | None = None
|
|
111
|
+
right: float | None = None
|
|
112
|
+
up: float | None = None
|
|
113
|
+
down: float | None = None
|
|
114
|
+
distance_from_entrance: float = 0.0
|
|
115
|
+
|
|
116
|
+
def __str__(self) -> str:
|
|
117
|
+
"""Format as PLT file syntax."""
|
|
118
|
+
cmd = "M" if self.operation == DrawOperation.MOVE_TO else "D"
|
|
119
|
+
result = f"{cmd}\t{self.location.northing}\t{self.location.easting}"
|
|
120
|
+
result += f"\t{self.location.vertical}"
|
|
121
|
+
if self.station_name:
|
|
122
|
+
result += f"\tS{self.station_name[:12]}"
|
|
123
|
+
result += "\tP"
|
|
124
|
+
result += f"\t{self.left if self.left is not None else -9.0}"
|
|
125
|
+
result += f"\t{self.up if self.up is not None else -9.0}"
|
|
126
|
+
result += f"\t{self.down if self.down is not None else -9.0}"
|
|
127
|
+
result += f"\t{self.right if self.right is not None else -9.0}"
|
|
128
|
+
result += f"\tI\t{self.distance_from_entrance}"
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class FeatureCommand(CompassPlotCommand):
|
|
133
|
+
"""Feature point command (L).
|
|
134
|
+
|
|
135
|
+
Represents a feature location with optional value.
|
|
136
|
+
|
|
137
|
+
Attributes:
|
|
138
|
+
location: 3D coordinates
|
|
139
|
+
station_name: Station identifier
|
|
140
|
+
left: Distance to left wall (feet, None if missing)
|
|
141
|
+
right: Distance to right wall (feet, None if missing)
|
|
142
|
+
up: Distance to ceiling (feet, None if missing)
|
|
143
|
+
down: Distance to floor (feet, None if missing)
|
|
144
|
+
value: Feature value
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
location: Location = Field(default_factory=Location)
|
|
148
|
+
station_name: str | None = None
|
|
149
|
+
left: float | None = None
|
|
150
|
+
right: float | None = None
|
|
151
|
+
up: float | None = None
|
|
152
|
+
down: float | None = None
|
|
153
|
+
value: Decimal | None = None
|
|
154
|
+
|
|
155
|
+
def __str__(self) -> str:
|
|
156
|
+
"""Format as PLT file syntax."""
|
|
157
|
+
result = f"L\t{self.location.northing}\t{self.location.easting}"
|
|
158
|
+
result += f"\t{self.location.vertical}"
|
|
159
|
+
if self.station_name:
|
|
160
|
+
result += f"\tS{self.station_name[:12]}"
|
|
161
|
+
result += "\tP"
|
|
162
|
+
result += f"\t{self.left if self.left is not None else -9.0}"
|
|
163
|
+
result += f"\t{self.right if self.right is not None else -9.0}"
|
|
164
|
+
result += f"\t{self.up if self.up is not None else -9.0}"
|
|
165
|
+
result += f"\t{self.down if self.down is not None else -9.0}"
|
|
166
|
+
if self.value is not None:
|
|
167
|
+
result += f"\tV\t{self.value}"
|
|
168
|
+
return result
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SurveyBoundsCommand(CompassPlotCommand):
|
|
172
|
+
"""Survey bounds command (X).
|
|
173
|
+
|
|
174
|
+
Defines the bounding box for the current survey.
|
|
175
|
+
|
|
176
|
+
Attributes:
|
|
177
|
+
bounds: Lower and upper bound locations
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
bounds: Bounds = Field(default_factory=Bounds)
|
|
181
|
+
|
|
182
|
+
def __str__(self) -> str:
|
|
183
|
+
"""Format as PLT file syntax."""
|
|
184
|
+
lb = self.bounds.lower
|
|
185
|
+
ub = self.bounds.upper
|
|
186
|
+
return (
|
|
187
|
+
f"X\t{lb.northing}\t{ub.northing}\t{lb.easting}"
|
|
188
|
+
f"\t{ub.easting}\t{lb.vertical}\t{ub.vertical}"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class CaveBoundsCommand(CompassPlotCommand):
|
|
193
|
+
"""Cave bounds command (Z).
|
|
194
|
+
|
|
195
|
+
Defines the overall bounding box for the cave.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
bounds: Lower and upper bound locations
|
|
199
|
+
distance_to_farthest_station: Distance to farthest station from entrance
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
bounds: Bounds = Field(default_factory=Bounds)
|
|
203
|
+
distance_to_farthest_station: float | None = None
|
|
204
|
+
|
|
205
|
+
def __str__(self) -> str:
|
|
206
|
+
"""Format as PLT file syntax."""
|
|
207
|
+
lb = self.bounds.lower
|
|
208
|
+
ub = self.bounds.upper
|
|
209
|
+
result = (
|
|
210
|
+
f"Z\t{lb.northing}\t{ub.northing}\t{lb.easting}"
|
|
211
|
+
f"\t{ub.easting}\t{lb.vertical}\t{ub.vertical}"
|
|
212
|
+
)
|
|
213
|
+
if self.distance_to_farthest_station is not None:
|
|
214
|
+
result += f"\tI\t{self.distance_to_farthest_station}"
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class DatumCommand(CompassPlotCommand):
|
|
219
|
+
"""Datum command (O).
|
|
220
|
+
|
|
221
|
+
Specifies the geodetic datum.
|
|
222
|
+
|
|
223
|
+
Attributes:
|
|
224
|
+
datum: Datum identifier
|
|
225
|
+
"""
|
|
226
|
+
|
|
227
|
+
datum: Datum
|
|
228
|
+
|
|
229
|
+
@field_validator("datum", mode="before")
|
|
230
|
+
@classmethod
|
|
231
|
+
def normalize_datum(cls, value: str | Datum) -> Datum:
|
|
232
|
+
"""Validate and normalize datum string to Datum enum.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
value: Datum as string or Datum enum
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Datum enum value
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: If datum string is not recognized
|
|
242
|
+
"""
|
|
243
|
+
if isinstance(value, Datum):
|
|
244
|
+
return value
|
|
245
|
+
return Datum.normalize(value)
|
|
246
|
+
|
|
247
|
+
def __str__(self) -> str:
|
|
248
|
+
"""Format as PLT file syntax."""
|
|
249
|
+
return f"O{self.datum.value}"
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
class UtmZoneCommand(CompassPlotCommand):
|
|
253
|
+
"""UTM zone command (G).
|
|
254
|
+
|
|
255
|
+
Specifies the UTM zone.
|
|
256
|
+
|
|
257
|
+
Attributes:
|
|
258
|
+
utm_zone: UTM zone identifier (stored as string)
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
utm_zone: str
|
|
262
|
+
|
|
263
|
+
def __str__(self) -> str:
|
|
264
|
+
"""Format as PLT file syntax."""
|
|
265
|
+
return f"G{self.utm_zone}"
|