compass-lib 0.0.1__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.
Files changed (38) hide show
  1. compass_lib/__init__.py +115 -3
  2. compass_lib/commands/__init__.py +2 -1
  3. compass_lib/commands/convert.py +225 -32
  4. compass_lib/commands/encrypt.py +115 -0
  5. compass_lib/commands/geojson.py +118 -0
  6. compass_lib/commands/main.py +4 -2
  7. compass_lib/constants.py +84 -0
  8. compass_lib/enums.py +309 -65
  9. compass_lib/errors.py +86 -0
  10. compass_lib/geo_utils.py +47 -0
  11. compass_lib/geojson.py +1024 -0
  12. compass_lib/interface.py +332 -0
  13. compass_lib/io.py +246 -0
  14. compass_lib/models.py +251 -0
  15. compass_lib/plot/__init__.py +28 -0
  16. compass_lib/plot/models.py +265 -0
  17. compass_lib/plot/parser.py +610 -0
  18. compass_lib/project/__init__.py +36 -0
  19. compass_lib/project/format.py +158 -0
  20. compass_lib/project/models.py +494 -0
  21. compass_lib/project/parser.py +638 -0
  22. compass_lib/survey/__init__.py +24 -0
  23. compass_lib/survey/format.py +284 -0
  24. compass_lib/survey/models.py +160 -0
  25. compass_lib/survey/parser.py +842 -0
  26. compass_lib/validation.py +74 -0
  27. compass_lib-0.0.3.dist-info/METADATA +60 -0
  28. compass_lib-0.0.3.dist-info/RECORD +31 -0
  29. {compass_lib-0.0.1.dist-info → compass_lib-0.0.3.dist-info}/WHEEL +1 -3
  30. compass_lib-0.0.3.dist-info/entry_points.txt +8 -0
  31. compass_lib/parser.py +0 -282
  32. compass_lib/section.py +0 -18
  33. compass_lib/shot.py +0 -21
  34. compass_lib-0.0.1.dist-info/METADATA +0 -268
  35. compass_lib-0.0.1.dist-info/RECORD +0 -14
  36. compass_lib-0.0.1.dist-info/entry_points.txt +0 -5
  37. compass_lib-0.0.1.dist-info/top_level.txt +0 -1
  38. {compass_lib-0.0.1.dist-info → compass_lib-0.0.3.dist-info/licenses}/LICENSE +0 -0
compass_lib/models.py ADDED
@@ -0,0 +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
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Annotated
11
+
12
+ from pydantic import BaseModel
13
+ from pydantic import Field
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.
133
+
134
+ Accepts either a string (which will be normalized) or a Datum enum value.
135
+
136
+ Args:
137
+ value: Datum as string or Datum enum, or None
138
+
139
+ Returns:
140
+ Datum enum value or None
141
+
142
+ Raises:
143
+ ValueError: If datum string is not recognized
144
+ """
145
+ if value is None or isinstance(value, Datum):
146
+ return value
147
+
148
+ # Normalize string to Datum enum
149
+ return Datum.normalize(value)
150
+
151
+ # -----------------------------
152
+ # Properties
153
+ # -----------------------------
154
+
155
+ @property
156
+ def is_northern_hemisphere(self) -> bool:
157
+ """Check if this location is in the northern hemisphere.
158
+
159
+ Returns:
160
+ True if northern hemisphere (zone > 0), False if southern (zone < 0)
161
+ """
162
+ return self.zone > 0
163
+
164
+ @property
165
+ def zone_number(self) -> int:
166
+ """Get the absolute zone number (1-60).
167
+
168
+ Returns:
169
+ Absolute value of the zone number
170
+ """
171
+ return abs(self.zone)
172
+
173
+ # -----------------------------
174
+ # Methods
175
+ # -----------------------------
176
+
177
+ def to_latlon(self) -> tuple[float, float]:
178
+ """
179
+ Convert this UTM location to GPS coordinates (latitude, longitude).
180
+
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.
185
+
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
189
+
190
+ Args:
191
+ None
192
+
193
+ Returns:
194
+ (lat, lon) in decimal degrees
195
+ """ # noqa: E501
196
+
197
+ WGS_1984_EPSG = 4326
198
+ geographic_crs = CRS.from_epsg(WGS_1984_EPSG)
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
+ )
207
+
208
+ transformer = Transformer.from_crs(
209
+ utm_crs,
210
+ geographic_crs,
211
+ always_xy=True,
212
+ )
213
+
214
+ lon, lat = transformer.transform(self.easting, self.northing)
215
+ return lat, lon
216
+
217
+
218
+ class Location(BaseModel):
219
+ """3D plot location with northing, easting, and vertical components.
220
+
221
+ Used for plot file commands. Values are in feet.
222
+
223
+ Attributes:
224
+ northing: North-South coordinate (feet)
225
+ easting: East-West coordinate (feet)
226
+ vertical: Vertical coordinate (feet)
227
+ """
228
+
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
+ )
240
+
241
+
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
+ """
249
+
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}"