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.
- compass_lib/__init__.py +115 -3
- compass_lib/commands/__init__.py +2 -1
- compass_lib/commands/convert.py +225 -32
- compass_lib/commands/encrypt.py +115 -0
- compass_lib/commands/geojson.py +118 -0
- compass_lib/commands/main.py +4 -2
- compass_lib/constants.py +84 -0
- compass_lib/enums.py +309 -65
- 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 +251 -0
- 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.3.dist-info/METADATA +60 -0
- compass_lib-0.0.3.dist-info/RECORD +31 -0
- {compass_lib-0.0.1.dist-info → compass_lib-0.0.3.dist-info}/WHEEL +1 -3
- compass_lib-0.0.3.dist-info/entry_points.txt +8 -0
- compass_lib/parser.py +0 -282
- compass_lib/section.py +0 -18
- compass_lib/shot.py +0 -21
- compass_lib-0.0.1.dist-info/METADATA +0 -268
- compass_lib-0.0.1.dist-info/RECORD +0 -14
- compass_lib-0.0.1.dist-info/entry_points.txt +0 -5
- compass_lib-0.0.1.dist-info/top_level.txt +0 -1
- {compass_lib-0.0.1.dist-info → compass_lib-0.0.3.dist-info/licenses}/LICENSE +0 -0
compass_lib/constants.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Constants used throughout the compass_lib library.
|
|
3
|
+
|
|
4
|
+
This module centralizes all constant values to ensure consistency
|
|
5
|
+
and avoid magic numbers/strings scattered across the codebase.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# -----------------------------------------------------------------------------
|
|
9
|
+
# File Encodings
|
|
10
|
+
# -----------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
#: Default encoding for Compass files (Windows-1252 / CP1252)
|
|
13
|
+
COMPASS_ENCODING = "cp1252"
|
|
14
|
+
|
|
15
|
+
#: Encoding used for JSON files
|
|
16
|
+
JSON_ENCODING = "utf-8"
|
|
17
|
+
|
|
18
|
+
#: Encoding used when reading raw Compass files (ASCII with replacements)
|
|
19
|
+
ASCII_ENCODING = "ascii"
|
|
20
|
+
|
|
21
|
+
# -----------------------------------------------------------------------------
|
|
22
|
+
# Unit Conversions
|
|
23
|
+
# -----------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
#: Conversion factor from feet to meters
|
|
26
|
+
FEET_TO_METERS: float = 0.3048
|
|
27
|
+
|
|
28
|
+
#: Conversion factor from meters to feet
|
|
29
|
+
METERS_TO_FEET: float = 1.0 / FEET_TO_METERS
|
|
30
|
+
|
|
31
|
+
# -----------------------------------------------------------------------------
|
|
32
|
+
# Missing Data Indicators
|
|
33
|
+
# -----------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
#: Values >= this threshold indicate missing data for distances/measurements
|
|
36
|
+
MISSING_VALUE_THRESHOLD: float = 990.0
|
|
37
|
+
|
|
38
|
+
#: Values <= this threshold indicate missing data for angles
|
|
39
|
+
MISSING_ANGLE_THRESHOLD: float = -900.0
|
|
40
|
+
|
|
41
|
+
#: String representation of missing value in formatted output
|
|
42
|
+
MISSING_VALUE_STRING: str = "-999.00"
|
|
43
|
+
|
|
44
|
+
#: Null LRUD values used in PLT files (either 999.0 or 999.9 indicates missing)
|
|
45
|
+
NULL_LRUD_VALUES: tuple[float, float] = (999.0, 999.9)
|
|
46
|
+
|
|
47
|
+
# -----------------------------------------------------------------------------
|
|
48
|
+
# Formatting Constants
|
|
49
|
+
# -----------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
#: Width of station name column in DAT files
|
|
52
|
+
STATION_NAME_WIDTH: int = 13
|
|
53
|
+
|
|
54
|
+
#: Width of numeric columns in DAT files
|
|
55
|
+
NUMBER_WIDTH: int = 8
|
|
56
|
+
|
|
57
|
+
#: Decimal precision for GeoJSON coordinates (WGS84)
|
|
58
|
+
GEOJSON_COORDINATE_PRECISION: int = 7
|
|
59
|
+
|
|
60
|
+
#: Decimal precision for elevation values in GeoJSON
|
|
61
|
+
GEOJSON_ELEVATION_PRECISION: int = 2
|
|
62
|
+
|
|
63
|
+
# -----------------------------------------------------------------------------
|
|
64
|
+
# Shot Flag Characters
|
|
65
|
+
# -----------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
#: Mapping of shot flags to their character representations
|
|
68
|
+
FLAG_CHARS: dict[str, str] = {
|
|
69
|
+
"exclude_distance": "L",
|
|
70
|
+
"exclude_from_plotting": "P",
|
|
71
|
+
"exclude_from_all_processing": "X",
|
|
72
|
+
"do_not_adjust": "C",
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# -----------------------------------------------------------------------------
|
|
76
|
+
# UTM Constants
|
|
77
|
+
# -----------------------------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
#: Southern hemisphere UTM northing offset (10 million meters)
|
|
80
|
+
#: Note: This constant represents the false northing added to southern hemisphere
|
|
81
|
+
#: UTM coordinates. However, in Compass, hemisphere is determined by the ZONE SIGN
|
|
82
|
+
#: (positive = north, negative = south), NOT by comparing northing to this threshold.
|
|
83
|
+
#: This constant is kept for reference and potential future use.
|
|
84
|
+
UTM_SOUTHERN_HEMISPHERE_OFFSET: float = 10_000_000.0
|
compass_lib/enums.py
CHANGED
|
@@ -1,112 +1,356 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Enumerations for Compass file formats.
|
|
3
|
+
|
|
4
|
+
This module contains all enumerations used in Compass survey data formats,
|
|
5
|
+
including units for measurements, file types, and various classification types.
|
|
6
|
+
"""
|
|
7
|
+
|
|
1
8
|
from enum import Enum
|
|
9
|
+
from math import radians
|
|
10
|
+
from math import tan
|
|
11
|
+
|
|
12
|
+
from compass_lib.constants import FEET_TO_METERS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FileFormat(str, Enum):
|
|
16
|
+
"""File format types for conversion operations.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
COMPASS: Native Compass binary/text format (.dat, .mak, .plt)
|
|
20
|
+
JSON: JSON serialization format
|
|
21
|
+
GEOJSON: GeoJSON geographic format
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
COMPASS = "compass"
|
|
25
|
+
JSON = "json"
|
|
26
|
+
GEOJSON = "geojson"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class CompassFileType(str, Enum):
|
|
30
|
+
"""Types of Compass files.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
DAT: Survey data file containing shots
|
|
34
|
+
MAK: Project/make file linking DAT files
|
|
35
|
+
PLT: Plot file with processed survey data
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
DAT = "dat"
|
|
39
|
+
MAK = "mak"
|
|
40
|
+
PLT = "plt"
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def extension(self) -> str:
|
|
44
|
+
"""Get the file extension for this type (with dot)."""
|
|
45
|
+
return {
|
|
46
|
+
CompassFileType.DAT: FileExtension.DAT.value,
|
|
47
|
+
CompassFileType.MAK: FileExtension.MAK.value,
|
|
48
|
+
CompassFileType.PLT: FileExtension.PLT.value,
|
|
49
|
+
}[self]
|
|
2
50
|
|
|
3
|
-
class CustomEnum(Enum):
|
|
4
51
|
@classmethod
|
|
5
|
-
def
|
|
6
|
-
|
|
52
|
+
def from_extension(cls, ext: str) -> "CompassFileType | None":
|
|
53
|
+
"""Get file type from extension.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
ext: File extension (with or without dot, case-insensitive)
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
CompassFileType or None if not recognized
|
|
60
|
+
"""
|
|
61
|
+
ext_lower = ext.lower().lstrip(".")
|
|
62
|
+
mapping = {
|
|
63
|
+
"dat": cls.DAT,
|
|
64
|
+
"mak": cls.MAK,
|
|
65
|
+
"plt": cls.PLT,
|
|
66
|
+
}
|
|
67
|
+
return mapping.get(ext_lower)
|
|
68
|
+
|
|
7
69
|
|
|
8
|
-
|
|
70
|
+
class FileExtension(str, Enum):
|
|
71
|
+
"""File extensions for various file formats (with dot).
|
|
9
72
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
73
|
+
Attributes:
|
|
74
|
+
DAT: Compass survey data file extension
|
|
75
|
+
MAK: Compass project/make file extension
|
|
76
|
+
PLT: Compass plot file extension
|
|
77
|
+
JSON: JSON file extension
|
|
78
|
+
GEOJSON: GeoJSON file extension
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
DAT = ".dat"
|
|
82
|
+
MAK = ".mak"
|
|
83
|
+
PLT = ".plt"
|
|
84
|
+
JSON = ".json"
|
|
85
|
+
GEOJSON = ".geojson"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FormatIdentifier(str, Enum):
|
|
89
|
+
"""Format identifiers used in JSON files.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
COMPASS_DAT: Format identifier for DAT files in JSON
|
|
93
|
+
COMPASS_MAK: Format identifier for MAK/project files in JSON
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
COMPASS_DAT = "compass_dat"
|
|
97
|
+
COMPASS_MAK = "compass_mak"
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AzimuthUnit(str, Enum):
|
|
101
|
+
"""Unit for compass azimuth (bearing) measurements.
|
|
102
|
+
|
|
103
|
+
Attributes:
|
|
104
|
+
DEGREES: Standard degrees (0-360)
|
|
105
|
+
QUADS: Quadrant notation
|
|
106
|
+
GRADS: Gradians (400 per circle)
|
|
107
|
+
"""
|
|
15
108
|
|
|
16
|
-
class AzimuthUnits(CustomEnum):
|
|
17
109
|
DEGREES = "D"
|
|
18
110
|
QUADS = "Q"
|
|
19
|
-
|
|
111
|
+
GRADS = "R"
|
|
112
|
+
|
|
113
|
+
@staticmethod
|
|
114
|
+
def convert(degrees: float | None, to_unit: "AzimuthUnit") -> float | None:
|
|
115
|
+
"""Convert degrees to the target unit.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
degrees: Value in degrees (or None)
|
|
119
|
+
to_unit: Target unit to convert to
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Converted value or None if input is None
|
|
123
|
+
"""
|
|
124
|
+
if degrees is None:
|
|
125
|
+
return None
|
|
126
|
+
if to_unit == AzimuthUnit.GRADS:
|
|
127
|
+
return degrees * 400 / 360
|
|
128
|
+
return degrees
|
|
20
129
|
|
|
21
|
-
# ============================== Inclination Unit ============================== #
|
|
22
130
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
131
|
+
class InclinationUnit(str, Enum):
|
|
132
|
+
"""Unit for vertical angle (inclination) measurements.
|
|
133
|
+
|
|
134
|
+
Attributes:
|
|
135
|
+
DEGREES: Standard degrees (-90 to +90)
|
|
136
|
+
PERCENT_GRADE: Percentage gradient (tan(angle) * 100)
|
|
137
|
+
DEGREES_AND_MINUTES: Degrees with minutes notation
|
|
138
|
+
GRADS: Gradians
|
|
139
|
+
DEPTH_GAUGE: Depth gauge reading
|
|
140
|
+
"""
|
|
30
141
|
|
|
31
|
-
class InclinationUnits(CustomEnum):
|
|
32
142
|
DEGREES = "D"
|
|
33
143
|
PERCENT_GRADE = "G"
|
|
34
144
|
DEGREES_AND_MINUTES = "M"
|
|
35
|
-
|
|
145
|
+
GRADS = "R"
|
|
36
146
|
DEPTH_GAUGE = "W"
|
|
37
147
|
|
|
38
|
-
|
|
148
|
+
@staticmethod
|
|
149
|
+
def convert(value: float | None, to_unit: "InclinationUnit") -> float | None:
|
|
150
|
+
"""Convert degrees to the target unit.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
value: Value in degrees (or None)
|
|
154
|
+
to_unit: Target unit to convert to
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Converted value or None if input is None
|
|
158
|
+
"""
|
|
159
|
+
if value is None:
|
|
160
|
+
return None
|
|
161
|
+
if to_unit == InclinationUnit.PERCENT_GRADE:
|
|
162
|
+
return tan(radians(value)) * 100
|
|
163
|
+
if to_unit == InclinationUnit.GRADS:
|
|
164
|
+
return value * 200 / 180
|
|
165
|
+
return value
|
|
39
166
|
|
|
40
167
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
168
|
+
class LengthUnit(str, Enum):
|
|
169
|
+
"""Unit for distance measurements.
|
|
170
|
+
|
|
171
|
+
Attributes:
|
|
172
|
+
DECIMAL_FEET: Standard feet with decimals
|
|
173
|
+
FEET_AND_INCHES: Feet and inches notation
|
|
174
|
+
METERS: Metric meters
|
|
175
|
+
"""
|
|
46
176
|
|
|
47
|
-
class LengthUnits(CustomEnum):
|
|
48
177
|
DECIMAL_FEET = "D"
|
|
49
178
|
FEET_AND_INCHES = "I"
|
|
50
179
|
METERS = "M"
|
|
51
180
|
|
|
52
|
-
|
|
181
|
+
@staticmethod
|
|
182
|
+
def convert(feet: float | None, to_unit: "LengthUnit") -> float | None:
|
|
183
|
+
"""Convert feet to the target unit.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
feet: Value in feet (or None)
|
|
187
|
+
to_unit: Target unit to convert to
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Converted value or None if input is None
|
|
191
|
+
"""
|
|
192
|
+
if feet is None:
|
|
193
|
+
return None
|
|
194
|
+
if to_unit == LengthUnit.METERS:
|
|
195
|
+
return feet * FEET_TO_METERS
|
|
196
|
+
return feet
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
class LrudAssociation(str, Enum):
|
|
200
|
+
"""Indicates which station LRUD measurements are associated with.
|
|
201
|
+
|
|
202
|
+
Attributes:
|
|
203
|
+
FROM: LRUD measured at the FROM station
|
|
204
|
+
TO: LRUD measured at the TO station
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
FROM = "F"
|
|
208
|
+
TO = "T"
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
class LrudItem(str, Enum):
|
|
212
|
+
"""Individual LRUD dimension identifiers.
|
|
53
213
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
214
|
+
Attributes:
|
|
215
|
+
LEFT: Distance to left wall
|
|
216
|
+
RIGHT: Distance to right wall
|
|
217
|
+
UP: Distance to ceiling
|
|
218
|
+
DOWN: Distance to floor
|
|
219
|
+
"""
|
|
60
220
|
|
|
61
|
-
class LRUD(CustomEnum):
|
|
62
221
|
LEFT = "L"
|
|
63
222
|
RIGHT = "R"
|
|
64
223
|
UP = "U"
|
|
65
224
|
DOWN = "D"
|
|
66
225
|
|
|
67
|
-
# ============================== ShotItem ============================== #
|
|
68
226
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
227
|
+
class ShotItem(str, Enum):
|
|
228
|
+
"""Components of a shot measurement for format ordering.
|
|
229
|
+
|
|
230
|
+
Attributes:
|
|
231
|
+
LENGTH: Distance between stations
|
|
232
|
+
FRONTSIGHT_AZIMUTH: Compass bearing forward
|
|
233
|
+
FRONTSIGHT_INCLINATION: Vertical angle forward
|
|
234
|
+
BACKSIGHT_AZIMUTH: Compass bearing backward
|
|
235
|
+
BACKSIGHT_INCLINATION: Vertical angle backward
|
|
236
|
+
"""
|
|
76
237
|
|
|
77
|
-
class ShotItem(CustomEnum):
|
|
78
238
|
LENGTH = "L"
|
|
79
239
|
FRONTSIGHT_AZIMUTH = "A"
|
|
80
240
|
FRONTSIGHT_INCLINATION = "D"
|
|
81
241
|
BACKSIGHT_AZIMUTH = "a"
|
|
82
242
|
BACKSIGHT_INCLINATION = "d"
|
|
83
243
|
|
|
84
|
-
# ============================== StationSide ============================== #
|
|
85
244
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
# T: 'to',
|
|
89
|
-
# }
|
|
245
|
+
class Severity(str, Enum):
|
|
246
|
+
"""Severity level for parse errors.
|
|
90
247
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
248
|
+
Attributes:
|
|
249
|
+
ERROR: Critical parsing error
|
|
250
|
+
WARNING: Non-fatal warning
|
|
251
|
+
"""
|
|
94
252
|
|
|
253
|
+
ERROR = "error"
|
|
254
|
+
WARNING = "warning"
|
|
95
255
|
|
|
96
|
-
# ============================== ShotFlag ============================== #
|
|
97
256
|
|
|
98
|
-
class
|
|
99
|
-
|
|
100
|
-
EXCLUDE_CLOSURE = "C"
|
|
101
|
-
EXCLUDE_LENGTH = "L"
|
|
102
|
-
TOTAL_EXCLUSION = "X"
|
|
103
|
-
SPLAY = "S"
|
|
257
|
+
class DrawOperation(str, Enum):
|
|
258
|
+
"""Drawing operations for plot commands.
|
|
104
259
|
|
|
105
|
-
|
|
106
|
-
|
|
260
|
+
Attributes:
|
|
261
|
+
MOVE_TO: Move to location without drawing
|
|
262
|
+
LINE_TO: Draw line to location
|
|
263
|
+
"""
|
|
107
264
|
|
|
265
|
+
MOVE_TO = "M"
|
|
266
|
+
LINE_TO = "D"
|
|
108
267
|
|
|
109
268
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
269
|
+
class Datum(str, Enum):
|
|
270
|
+
"""Geodetic datum values supported by Compass.
|
|
271
|
+
|
|
272
|
+
These are the standard datum values that can be used in MAK project files.
|
|
273
|
+
The enum values match the exact strings used in Compass MAK files.
|
|
274
|
+
|
|
275
|
+
Attributes:
|
|
276
|
+
ADINDAN: Adindan datum
|
|
277
|
+
ARC_1950: Arc 1950 datum
|
|
278
|
+
ARC_1960: Arc 1960 datum
|
|
279
|
+
AUSTRALIAN_1966: Australian 1966 datum
|
|
280
|
+
AUSTRALIAN_1984: Australian 1984 datum
|
|
281
|
+
CAMP_AREA_ASTRO: Camp Area Astro datum
|
|
282
|
+
CAPE: Cape datum
|
|
283
|
+
EUROPEAN_1950: European 1950 datum
|
|
284
|
+
EUROPEAN_1979: European 1979 datum
|
|
285
|
+
GEODETIC_1949: Geodetic 1949 datum
|
|
286
|
+
HONG_KONG_1963: Hong Kong 1963 datum
|
|
287
|
+
HU_TZU_SHAN: Hu Tzu Shan datum
|
|
288
|
+
INDIAN: Indian datum
|
|
289
|
+
NORTH_AMERICAN_1927: North American 1927 datum (NAD27)
|
|
290
|
+
NORTH_AMERICAN_1983: North American 1983 datum (NAD83)
|
|
291
|
+
OMAN: Oman datum
|
|
292
|
+
ORDNANCE_SURVEY_1936: Ordnance Survey 1936 datum
|
|
293
|
+
PULKOVO_1942: Pulkovo 1942 datum
|
|
294
|
+
SOUTH_AMERICAN_1956: South American 1956 datum
|
|
295
|
+
SOUTH_AMERICAN_1969: South American 1969 datum
|
|
296
|
+
TOKYO: Tokyo datum
|
|
297
|
+
WGS_1972: WGS 1972 datum
|
|
298
|
+
WGS_1984: WGS 1984 datum
|
|
299
|
+
"""
|
|
300
|
+
|
|
301
|
+
ADINDAN = "Adindan"
|
|
302
|
+
ARC_1950 = "Arc 1950"
|
|
303
|
+
ARC_1960 = "Arc 1960"
|
|
304
|
+
AUSTRALIAN_1966 = "Australian 1966"
|
|
305
|
+
AUSTRALIAN_1984 = "Australian 1984"
|
|
306
|
+
CAMP_AREA_ASTRO = "Camp Area Astro"
|
|
307
|
+
CAPE = "Cape"
|
|
308
|
+
EUROPEAN_1950 = "European 1950"
|
|
309
|
+
EUROPEAN_1979 = "European 1979"
|
|
310
|
+
GEODETIC_1949 = "Geodetic 1949"
|
|
311
|
+
HONG_KONG_1963 = "Hong Kong 1963"
|
|
312
|
+
HU_TZU_SHAN = "Hu Tzu Shan"
|
|
313
|
+
INDIAN = "Indian"
|
|
314
|
+
NORTH_AMERICAN_1927 = "North American 1927"
|
|
315
|
+
NORTH_AMERICAN_1983 = "North American 1983"
|
|
316
|
+
OMAN = "Oman"
|
|
317
|
+
ORDNANCE_SURVEY_1936 = "Ordnance Survey 1936"
|
|
318
|
+
PULKOVO_1942 = "Pulkovo 1942"
|
|
319
|
+
SOUTH_AMERICAN_1956 = "South American 1956"
|
|
320
|
+
SOUTH_AMERICAN_1969 = "South American 1969"
|
|
321
|
+
TOKYO = "Tokyo"
|
|
322
|
+
WGS_1972 = "WGS 1972"
|
|
323
|
+
WGS_1984 = "WGS 1984"
|
|
324
|
+
|
|
325
|
+
@classmethod
|
|
326
|
+
def normalize(cls, value: str | None) -> "Datum | None":
|
|
327
|
+
"""Normalize and validate a datum string to a Datum enum value.
|
|
328
|
+
|
|
329
|
+
Performs case-insensitive matching with whitespace normalization.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
value: The datum string to normalize (case-insensitive)
|
|
333
|
+
|
|
334
|
+
Returns:
|
|
335
|
+
The corresponding Datum enum value, or None if value is None
|
|
336
|
+
|
|
337
|
+
Raises:
|
|
338
|
+
ValueError: If the datum string is not recognized
|
|
339
|
+
"""
|
|
340
|
+
if value is None:
|
|
341
|
+
return None
|
|
342
|
+
|
|
343
|
+
# Normalize: lowercase, strip whitespace, collapse multiple spaces
|
|
344
|
+
normalized = " ".join(value.strip().lower().split())
|
|
345
|
+
|
|
346
|
+
# Match against enum values (case-insensitive)
|
|
347
|
+
for datum in cls:
|
|
348
|
+
if datum.value.lower() == normalized:
|
|
349
|
+
return datum
|
|
350
|
+
|
|
351
|
+
raise ValueError(f"Unknown datum: {value!r}")
|
|
352
|
+
|
|
353
|
+
@classmethod
|
|
354
|
+
def from_string(cls, value: str | None) -> "Datum | None":
|
|
355
|
+
"""Alias for normalize() for backwards compatibility."""
|
|
356
|
+
return cls.normalize(value)
|
compass_lib/errors.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Error handling for Compass file parsing.
|
|
3
|
+
|
|
4
|
+
This module provides error classes for tracking parsing errors with
|
|
5
|
+
source location information for helpful error messages.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
|
|
10
|
+
from compass_lib.enums import Severity
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class SourceLocation:
|
|
15
|
+
"""Tracks the source location of text for error reporting.
|
|
16
|
+
|
|
17
|
+
Attributes:
|
|
18
|
+
source: The source file name or identifier
|
|
19
|
+
line: Line number (0-based)
|
|
20
|
+
column: Column number (0-based)
|
|
21
|
+
text: The text at this location
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
source: str
|
|
25
|
+
line: int
|
|
26
|
+
column: int
|
|
27
|
+
text: str
|
|
28
|
+
|
|
29
|
+
def __str__(self) -> str:
|
|
30
|
+
"""Format as human-readable location string."""
|
|
31
|
+
return f"(in {self.source}, line {self.line + 1}, column {self.column + 1})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class CompassParseError:
|
|
36
|
+
"""Represents a parsing error or warning with source location.
|
|
37
|
+
|
|
38
|
+
This is a data record for storing error information, not an exception.
|
|
39
|
+
Use CompassParseException for raising errors.
|
|
40
|
+
|
|
41
|
+
Attributes:
|
|
42
|
+
severity: ERROR or WARNING
|
|
43
|
+
message: Human-readable error message
|
|
44
|
+
location: Source location where error occurred (optional)
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
severity: Severity
|
|
48
|
+
message: str
|
|
49
|
+
location: SourceLocation | None = None
|
|
50
|
+
|
|
51
|
+
def __str__(self) -> str:
|
|
52
|
+
"""Format as human-readable error string."""
|
|
53
|
+
base = f"{self.severity.value}: {self.message}"
|
|
54
|
+
if self.location:
|
|
55
|
+
base += f" {self.location}"
|
|
56
|
+
if self.location.text:
|
|
57
|
+
base += f"\n {self.location.text}"
|
|
58
|
+
return base
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CompassParseException(Exception): # noqa: N818
|
|
62
|
+
"""Exception raised for critical parsing errors.
|
|
63
|
+
|
|
64
|
+
Attributes:
|
|
65
|
+
message: Error message
|
|
66
|
+
location: Source location where error occurred
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, message: str, location: SourceLocation | None = None):
|
|
70
|
+
self.message = message
|
|
71
|
+
self.location = location
|
|
72
|
+
super().__init__(str(self))
|
|
73
|
+
|
|
74
|
+
def __str__(self) -> str:
|
|
75
|
+
"""Format as human-readable exception string."""
|
|
76
|
+
if self.location:
|
|
77
|
+
return f"{self.message} {self.location}"
|
|
78
|
+
return self.message
|
|
79
|
+
|
|
80
|
+
def to_error(self) -> CompassParseError:
|
|
81
|
+
"""Convert exception to CompassParseError record."""
|
|
82
|
+
return CompassParseError(
|
|
83
|
+
severity=Severity.ERROR,
|
|
84
|
+
message=self.message,
|
|
85
|
+
location=self.location,
|
|
86
|
+
)
|
compass_lib/geo_utils.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
|
|
5
|
+
import pyIGRF14 as pyIGRF
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
from pydantic_extra_types.coordinate import Latitude # noqa: TC002
|
|
8
|
+
from pydantic_extra_types.coordinate import Longitude # noqa: TC002
|
|
9
|
+
|
|
10
|
+
from compass_lib.constants import GEOJSON_COORDINATE_PRECISION
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GeoLocation(BaseModel):
|
|
14
|
+
latitude: Latitude
|
|
15
|
+
longitude: Longitude
|
|
16
|
+
|
|
17
|
+
def as_tuple(self) -> tuple[float, float]:
|
|
18
|
+
"""Return the latitude and longitude as a tuple.
|
|
19
|
+
# RFC 7946: (longitude, latitude)
|
|
20
|
+
"""
|
|
21
|
+
return (
|
|
22
|
+
round(self.longitude, GEOJSON_COORDINATE_PRECISION),
|
|
23
|
+
round(self.latitude, GEOJSON_COORDINATE_PRECISION),
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def decimal_year(dt: datetime.datetime) -> float:
|
|
28
|
+
dt_start = datetime.datetime( # noqa: DTZ001
|
|
29
|
+
year=dt.year, month=1, day=1, hour=0, minute=0, second=0
|
|
30
|
+
)
|
|
31
|
+
dt_end = datetime.datetime( # noqa: DTZ001
|
|
32
|
+
year=dt.year + 1, month=1, day=1, hour=0, minute=0, second=0
|
|
33
|
+
)
|
|
34
|
+
return round(
|
|
35
|
+
dt.year + (dt - dt_start).total_seconds() / (dt_end - dt_start).total_seconds(),
|
|
36
|
+
ndigits=2,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_declination(location: GeoLocation, dt: datetime.datetime) -> float:
|
|
41
|
+
declination, _, _, _, _, _, _ = pyIGRF.igrf_value(
|
|
42
|
+
location.latitude,
|
|
43
|
+
location.longitude,
|
|
44
|
+
alt=0.0,
|
|
45
|
+
year=decimal_year(dt),
|
|
46
|
+
)
|
|
47
|
+
return round(declination, 2)
|