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
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Parser for Compass .MAK project files.
|
|
3
|
+
|
|
4
|
+
This module implements the parser for reading Compass project files,
|
|
5
|
+
which define project structure, geographic settings, and data file references.
|
|
6
|
+
|
|
7
|
+
Architecture: The parser produces dictionaries (like loading JSON) which are
|
|
8
|
+
then fed to Pydantic models via a single `model_validate()` call. This keeps
|
|
9
|
+
parsing logic separate from model construction.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from compass_lib.constants import ASCII_ENCODING
|
|
17
|
+
from compass_lib.enums import FormatIdentifier
|
|
18
|
+
from compass_lib.errors import CompassParseException
|
|
19
|
+
from compass_lib.errors import SourceLocation
|
|
20
|
+
from compass_lib.project.models import CompassMakFile
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CompassProjectParser:
|
|
24
|
+
"""Parser for Compass .MAK project files.
|
|
25
|
+
|
|
26
|
+
This parser reads Compass project files and produces dictionaries
|
|
27
|
+
(like loading JSON from disk). The dictionaries can then be fed to
|
|
28
|
+
Pydantic models via a single `model_validate()` call.
|
|
29
|
+
|
|
30
|
+
The parser is strict - it raises CompassParseException on errors.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Regex patterns
|
|
34
|
+
EOL_PATTERN = re.compile(r"\r\n?|\n")
|
|
35
|
+
FILE_NAME_PATTERN = re.compile(r"[^,;/]+")
|
|
36
|
+
DATUM_PATTERN = re.compile(r"[^;/]+")
|
|
37
|
+
LINK_STATION_PATTERN = re.compile(r"[^,;/\[]+")
|
|
38
|
+
NUMBER_PATTERN = re.compile(r"[-+]?\d+(\.\d*)?|\.\d+")
|
|
39
|
+
|
|
40
|
+
def __init__(self) -> None:
|
|
41
|
+
"""Initialize the parser."""
|
|
42
|
+
self._data: str = ""
|
|
43
|
+
self._pos: int = 0
|
|
44
|
+
self._source: str = "<string>"
|
|
45
|
+
self._line: int = 0
|
|
46
|
+
|
|
47
|
+
# -------------------------------------------------------------------------
|
|
48
|
+
# Dictionary-returning methods (primary API)
|
|
49
|
+
# -------------------------------------------------------------------------
|
|
50
|
+
|
|
51
|
+
def parse_file_to_dict(self, path: Path) -> dict[str, Any]:
|
|
52
|
+
"""Parse a project file to dictionary.
|
|
53
|
+
|
|
54
|
+
This is the primary parsing method. It returns a dictionary that
|
|
55
|
+
can be directly fed to `CompassMakFile.model_validate()`.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
path: Path to the .MAK file
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Dictionary with directives list
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
CompassParseException: On parse errors
|
|
65
|
+
"""
|
|
66
|
+
with path.open(mode="r", encoding=ASCII_ENCODING, errors="replace") as f:
|
|
67
|
+
data = f.read()
|
|
68
|
+
return self.parse_string_to_dict(data, str(path))
|
|
69
|
+
|
|
70
|
+
def parse_string_to_dict(
|
|
71
|
+
self,
|
|
72
|
+
data: str,
|
|
73
|
+
source: str = "<string>",
|
|
74
|
+
) -> dict[str, Any]:
|
|
75
|
+
"""Parse project data from a string to dictionary.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
data: Project file content
|
|
79
|
+
source: Source identifier for error messages
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Dictionary with directives list
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
CompassParseException: On parse errors
|
|
86
|
+
"""
|
|
87
|
+
self._data = data
|
|
88
|
+
self._pos = 0
|
|
89
|
+
self._source = source
|
|
90
|
+
self._line = 0
|
|
91
|
+
|
|
92
|
+
directives: list[dict[str, Any]] = []
|
|
93
|
+
|
|
94
|
+
while self._pos < len(self._data):
|
|
95
|
+
self._skip_whitespace()
|
|
96
|
+
if self._pos >= len(self._data):
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
char = self._data[self._pos]
|
|
100
|
+
self._pos += 1
|
|
101
|
+
|
|
102
|
+
if char == "#":
|
|
103
|
+
directives.append(self._parse_survey_file_to_dict())
|
|
104
|
+
elif char == "@":
|
|
105
|
+
directives.append(self._parse_location_to_dict())
|
|
106
|
+
elif char == "&":
|
|
107
|
+
directives.append(self._parse_datum_to_dict())
|
|
108
|
+
elif char == "%":
|
|
109
|
+
directives.append(self._parse_utm_convergence_to_dict(enabled=True))
|
|
110
|
+
elif char == "*":
|
|
111
|
+
directives.append(self._parse_utm_convergence_to_dict(enabled=False))
|
|
112
|
+
elif char == "$":
|
|
113
|
+
directives.append(self._parse_utm_zone_to_dict())
|
|
114
|
+
elif char == "!":
|
|
115
|
+
directives.append(self._parse_flags_to_dict())
|
|
116
|
+
elif char == "[":
|
|
117
|
+
directives.append(self._parse_folder_start_to_dict())
|
|
118
|
+
elif char == "]":
|
|
119
|
+
directives.append(self._parse_folder_end_to_dict())
|
|
120
|
+
elif char == "/":
|
|
121
|
+
directives.append(self._parse_comment_to_dict())
|
|
122
|
+
elif ord(char) >= 0x20:
|
|
123
|
+
# Unknown directive - parse to semicolon for roundtrip fidelity
|
|
124
|
+
directives.append(self._parse_unknown_directive_to_dict(char))
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
"version": "1.0",
|
|
128
|
+
"format": FormatIdentifier.COMPASS_MAK.value,
|
|
129
|
+
"directives": directives,
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# -------------------------------------------------------------------------
|
|
133
|
+
# Legacy model-returning methods (thin wrappers for backwards compat)
|
|
134
|
+
# -------------------------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
def parse_file(self, path: Path) -> list["CompassProjectDirective"]: # noqa: F821
|
|
137
|
+
"""Parse a project file.
|
|
138
|
+
|
|
139
|
+
DEPRECATED: Use parse_file_to_dict() for new code.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
path: Path to the .MAK file
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
List of parsed directives
|
|
146
|
+
|
|
147
|
+
Raises:
|
|
148
|
+
CompassParseException: On parse errors
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
data = self.parse_file_to_dict(path)
|
|
152
|
+
mak_file = CompassMakFile.model_validate(data)
|
|
153
|
+
return mak_file.directives
|
|
154
|
+
|
|
155
|
+
def parse_string(
|
|
156
|
+
self,
|
|
157
|
+
data: str,
|
|
158
|
+
source: str = "<string>",
|
|
159
|
+
) -> list["CompassProjectDirective"]: # noqa: F821
|
|
160
|
+
"""Parse project data from a string.
|
|
161
|
+
|
|
162
|
+
DEPRECATED: Use parse_string_to_dict() for new code.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
data: Project file content
|
|
166
|
+
source: Source identifier for error messages
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
List of parsed directives
|
|
170
|
+
|
|
171
|
+
Raises:
|
|
172
|
+
CompassParseException: On parse errors
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
parsed = self.parse_string_to_dict(data, source)
|
|
176
|
+
mak_file = CompassMakFile.model_validate(parsed)
|
|
177
|
+
return mak_file.directives
|
|
178
|
+
|
|
179
|
+
def _location(self, text: str = "") -> SourceLocation:
|
|
180
|
+
"""Create a SourceLocation at current position."""
|
|
181
|
+
return SourceLocation(
|
|
182
|
+
source=self._source,
|
|
183
|
+
line=self._line,
|
|
184
|
+
column=self._pos - 1,
|
|
185
|
+
text=text,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
def _skip_whitespace(self) -> None:
|
|
189
|
+
"""Skip whitespace characters, tracking line numbers."""
|
|
190
|
+
while self._pos < len(self._data):
|
|
191
|
+
char = self._data[self._pos]
|
|
192
|
+
if char == "\n":
|
|
193
|
+
self._line += 1
|
|
194
|
+
self._pos += 1
|
|
195
|
+
elif char == "\r":
|
|
196
|
+
self._line += 1
|
|
197
|
+
self._pos += 1
|
|
198
|
+
if self._pos < len(self._data) and self._data[self._pos] == "\n":
|
|
199
|
+
self._pos += 1
|
|
200
|
+
elif char.isspace():
|
|
201
|
+
self._pos += 1
|
|
202
|
+
else:
|
|
203
|
+
break
|
|
204
|
+
|
|
205
|
+
def _skip_whitespace_and_comments(self) -> None:
|
|
206
|
+
"""Skip whitespace and inline comments."""
|
|
207
|
+
self._skip_whitespace()
|
|
208
|
+
while self._pos < len(self._data) and self._data[self._pos] == "/":
|
|
209
|
+
self._pos += 1
|
|
210
|
+
self._skip_to_end_of_line()
|
|
211
|
+
self._skip_whitespace()
|
|
212
|
+
|
|
213
|
+
def _skip_to_end_of_line(self) -> None:
|
|
214
|
+
"""Skip to end of current line."""
|
|
215
|
+
while self._pos < len(self._data):
|
|
216
|
+
char = self._data[self._pos]
|
|
217
|
+
if char == "\n":
|
|
218
|
+
self._line += 1
|
|
219
|
+
self._pos += 1
|
|
220
|
+
break
|
|
221
|
+
if char == "\r":
|
|
222
|
+
self._line += 1
|
|
223
|
+
self._pos += 1
|
|
224
|
+
if self._pos < len(self._data) and self._data[self._pos] == "\n":
|
|
225
|
+
self._pos += 1
|
|
226
|
+
break
|
|
227
|
+
self._pos += 1
|
|
228
|
+
|
|
229
|
+
def _expect(self, char: str) -> None:
|
|
230
|
+
"""Expect a specific character.
|
|
231
|
+
|
|
232
|
+
Raises:
|
|
233
|
+
CompassParseError: If character doesn't match
|
|
234
|
+
"""
|
|
235
|
+
if self._pos >= len(self._data):
|
|
236
|
+
raise CompassParseException(
|
|
237
|
+
f"expected {char}, got end of file",
|
|
238
|
+
self._location(),
|
|
239
|
+
)
|
|
240
|
+
if self._data[self._pos] != char:
|
|
241
|
+
raise CompassParseException(
|
|
242
|
+
f"expected {char}, got {self._data[self._pos]}",
|
|
243
|
+
self._location(self._data[self._pos]),
|
|
244
|
+
)
|
|
245
|
+
self._pos += 1
|
|
246
|
+
|
|
247
|
+
def _expect_match(
|
|
248
|
+
self,
|
|
249
|
+
pattern: re.Pattern[str],
|
|
250
|
+
error_msg: str,
|
|
251
|
+
) -> str:
|
|
252
|
+
"""Match a pattern at current position.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
pattern: Regex pattern to match
|
|
256
|
+
error_msg: Error message if no match
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Matched text
|
|
260
|
+
|
|
261
|
+
Raises:
|
|
262
|
+
CompassParseError: If no match
|
|
263
|
+
"""
|
|
264
|
+
match = pattern.match(self._data, self._pos)
|
|
265
|
+
if not match or match.start() != self._pos:
|
|
266
|
+
raise CompassParseException(error_msg, self._location())
|
|
267
|
+
self._pos = match.end()
|
|
268
|
+
return match.group()
|
|
269
|
+
|
|
270
|
+
def _expect_number(self, error_msg: str) -> float:
|
|
271
|
+
"""Parse a number at current position.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
error_msg: Error message if no number
|
|
275
|
+
|
|
276
|
+
Returns:
|
|
277
|
+
Parsed number
|
|
278
|
+
|
|
279
|
+
Raises:
|
|
280
|
+
CompassParseError: If no valid number
|
|
281
|
+
"""
|
|
282
|
+
text = self._expect_match(self.NUMBER_PATTERN, error_msg)
|
|
283
|
+
return float(text)
|
|
284
|
+
|
|
285
|
+
def _expect_utm_zone(self, *, allow_zero: bool = False) -> int:
|
|
286
|
+
"""Parse UTM zone (integer 1-60 or -1 to -60, or 0 if allow_zero).
|
|
287
|
+
|
|
288
|
+
Positive zones indicate northern hemisphere.
|
|
289
|
+
Negative zones indicate southern hemisphere.
|
|
290
|
+
|
|
291
|
+
Args:
|
|
292
|
+
allow_zero: If True, allows zone 0 (meaning "not specified")
|
|
293
|
+
|
|
294
|
+
Returns:
|
|
295
|
+
UTM zone number
|
|
296
|
+
|
|
297
|
+
Raises:
|
|
298
|
+
CompassParseError: If invalid zone
|
|
299
|
+
"""
|
|
300
|
+
text = self._expect_match(self.NUMBER_PATTERN, "missing UTM zone")
|
|
301
|
+
|
|
302
|
+
if "." in text:
|
|
303
|
+
raise CompassParseException(
|
|
304
|
+
"invalid UTM zone (not an integer)", self._location(text)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
zone = int(text)
|
|
308
|
+
|
|
309
|
+
# Check if zone is valid
|
|
310
|
+
if zone == 0 and not allow_zero:
|
|
311
|
+
raise CompassParseException(
|
|
312
|
+
"UTM zone cannot be 0. Use 1-60 for north, -1 to -60 for south.",
|
|
313
|
+
self._location(text),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
if abs(zone) > 60:
|
|
317
|
+
raise CompassParseException(
|
|
318
|
+
f"UTM zone must be between -60 and 60, got {zone}",
|
|
319
|
+
self._location(text),
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
return zone
|
|
323
|
+
|
|
324
|
+
def _parse_length_unit(self) -> str:
|
|
325
|
+
"""Parse length unit (f or m).
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
'f' for feet, 'm' for meters
|
|
329
|
+
|
|
330
|
+
Raises:
|
|
331
|
+
CompassParseError: If invalid unit
|
|
332
|
+
"""
|
|
333
|
+
if self._pos >= len(self._data):
|
|
334
|
+
raise CompassParseException("missing length unit", self._location())
|
|
335
|
+
|
|
336
|
+
char = self._data[self._pos].lower()
|
|
337
|
+
if char not in ("f", "m"):
|
|
338
|
+
raise CompassParseException(
|
|
339
|
+
f"invalid length unit: {char}",
|
|
340
|
+
self._location(char),
|
|
341
|
+
)
|
|
342
|
+
self._pos += 1
|
|
343
|
+
return char
|
|
344
|
+
|
|
345
|
+
def _parse_survey_file_to_dict(self) -> dict[str, Any]:
|
|
346
|
+
"""Parse #filename,station[location],...; to dictionary."""
|
|
347
|
+
file_name = self._expect_match(
|
|
348
|
+
self.FILE_NAME_PATTERN,
|
|
349
|
+
"missing file name",
|
|
350
|
+
).strip()
|
|
351
|
+
|
|
352
|
+
link_stations: list[dict[str, Any]] = []
|
|
353
|
+
|
|
354
|
+
while True:
|
|
355
|
+
self._skip_whitespace_and_comments()
|
|
356
|
+
|
|
357
|
+
if self._pos >= len(self._data):
|
|
358
|
+
raise CompassParseException(
|
|
359
|
+
"missing ; at end of file line",
|
|
360
|
+
self._location(),
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
char = self._data[self._pos]
|
|
364
|
+
self._pos += 1
|
|
365
|
+
|
|
366
|
+
if char == ";":
|
|
367
|
+
return {
|
|
368
|
+
"type": "file",
|
|
369
|
+
"file": file_name,
|
|
370
|
+
"link_stations": link_stations,
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if char == ",":
|
|
374
|
+
self._skip_whitespace_and_comments()
|
|
375
|
+
station_name = self._expect_match(
|
|
376
|
+
self.LINK_STATION_PATTERN,
|
|
377
|
+
"missing station name",
|
|
378
|
+
).strip()
|
|
379
|
+
|
|
380
|
+
location = None
|
|
381
|
+
self._skip_whitespace_and_comments()
|
|
382
|
+
|
|
383
|
+
if self._pos < len(self._data) and self._data[self._pos] == "[":
|
|
384
|
+
self._pos += 1
|
|
385
|
+
self._skip_whitespace_and_comments()
|
|
386
|
+
|
|
387
|
+
unit = self._parse_length_unit()
|
|
388
|
+
self._skip_whitespace_and_comments()
|
|
389
|
+
self._expect(",")
|
|
390
|
+
self._skip_whitespace_and_comments()
|
|
391
|
+
|
|
392
|
+
easting = self._expect_number("missing easting")
|
|
393
|
+
self._skip_whitespace_and_comments()
|
|
394
|
+
self._expect(",")
|
|
395
|
+
self._skip_whitespace_and_comments()
|
|
396
|
+
|
|
397
|
+
northing = self._expect_number("missing northing")
|
|
398
|
+
self._skip_whitespace_and_comments()
|
|
399
|
+
self._expect(",")
|
|
400
|
+
self._skip_whitespace_and_comments()
|
|
401
|
+
|
|
402
|
+
elevation = self._expect_number("missing elevation")
|
|
403
|
+
self._skip_whitespace_and_comments()
|
|
404
|
+
self._expect("]")
|
|
405
|
+
|
|
406
|
+
location = {
|
|
407
|
+
"easting": easting,
|
|
408
|
+
"northing": northing,
|
|
409
|
+
"elevation": elevation,
|
|
410
|
+
"unit": unit,
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
link_stations.append({"name": station_name, "location": location})
|
|
414
|
+
else:
|
|
415
|
+
raise CompassParseException(
|
|
416
|
+
f"unexpected character: {char}",
|
|
417
|
+
self._location(char),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
def _parse_location_to_dict(self) -> dict[str, Any]:
|
|
421
|
+
"""Parse @easting,northing,elevation,zone,convergence; to dictionary.
|
|
422
|
+
|
|
423
|
+
Note: UTM zone of 0 is allowed here, meaning "no location specified"
|
|
424
|
+
(commonly used when only declination information is needed).
|
|
425
|
+
"""
|
|
426
|
+
self._skip_whitespace()
|
|
427
|
+
easting = self._expect_number("missing easting")
|
|
428
|
+
self._skip_whitespace()
|
|
429
|
+
self._expect(",")
|
|
430
|
+
self._skip_whitespace()
|
|
431
|
+
northing = self._expect_number("missing northing")
|
|
432
|
+
self._skip_whitespace()
|
|
433
|
+
self._expect(",")
|
|
434
|
+
self._skip_whitespace()
|
|
435
|
+
elevation = self._expect_number("missing elevation")
|
|
436
|
+
self._skip_whitespace()
|
|
437
|
+
self._expect(",")
|
|
438
|
+
self._skip_whitespace()
|
|
439
|
+
utm_zone = self._expect_utm_zone(allow_zero=True)
|
|
440
|
+
self._skip_whitespace()
|
|
441
|
+
self._expect(",")
|
|
442
|
+
self._skip_whitespace()
|
|
443
|
+
convergence = self._expect_number("missing UTM convergence")
|
|
444
|
+
self._expect(";")
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
"type": "location",
|
|
448
|
+
"easting": easting,
|
|
449
|
+
"northing": northing,
|
|
450
|
+
"elevation": elevation,
|
|
451
|
+
"utm_zone": utm_zone,
|
|
452
|
+
"utm_convergence": convergence,
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
def _parse_datum_to_dict(self) -> dict[str, Any]:
|
|
456
|
+
"""Parse &datum_name; to dictionary."""
|
|
457
|
+
datum = self._expect_match(self.DATUM_PATTERN, "missing datum").strip()
|
|
458
|
+
self._expect(";")
|
|
459
|
+
return {"type": "datum", "datum": datum}
|
|
460
|
+
|
|
461
|
+
def _parse_utm_convergence_to_dict(self, *, enabled: bool) -> dict[str, Any]:
|
|
462
|
+
"""Parse %convergence; or *convergence; to dictionary.
|
|
463
|
+
|
|
464
|
+
Args:
|
|
465
|
+
enabled: True if parsed from %, False if parsed from *
|
|
466
|
+
"""
|
|
467
|
+
self._skip_whitespace()
|
|
468
|
+
convergence = self._expect_number("missing UTM convergence")
|
|
469
|
+
self._expect(";")
|
|
470
|
+
return {
|
|
471
|
+
"type": "utm_convergence",
|
|
472
|
+
"utm_convergence": convergence,
|
|
473
|
+
"enabled": enabled,
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
def _parse_utm_zone_to_dict(self) -> dict[str, Any]:
|
|
477
|
+
"""Parse $zone; to dictionary."""
|
|
478
|
+
self._skip_whitespace()
|
|
479
|
+
zone = self._expect_utm_zone()
|
|
480
|
+
self._expect(";")
|
|
481
|
+
return {"type": "utm_zone", "utm_zone": zone}
|
|
482
|
+
|
|
483
|
+
def _parse_flags_to_dict(self) -> dict[str, Any]:
|
|
484
|
+
"""Parse !flags; to dictionary.
|
|
485
|
+
|
|
486
|
+
Parses all 10 documented Compass project flags:
|
|
487
|
+
1. G/g - Global override settings enabled/disabled
|
|
488
|
+
2. I/E/A - Declination mode (Ignore/Entered/Auto)
|
|
489
|
+
3. V/v - Apply UTM convergence enabled/disabled
|
|
490
|
+
4. O/o - Override LRUD associations enabled/disabled
|
|
491
|
+
5. T/t - LRUDs at To/From station
|
|
492
|
+
6. S/s - Apply shot flags enabled/disabled
|
|
493
|
+
7. X/x - Apply total exclusion flags enabled/disabled
|
|
494
|
+
8. P/p - Apply plotting exclusion flags enabled/disabled
|
|
495
|
+
9. L/l - Apply length exclusion flags enabled/disabled
|
|
496
|
+
10. C/c - Apply close exclusion flags enabled/disabled
|
|
497
|
+
"""
|
|
498
|
+
start_pos = self._pos
|
|
499
|
+
|
|
500
|
+
# Initialize all flags
|
|
501
|
+
result: dict[str, Any] = {
|
|
502
|
+
"type": "flags",
|
|
503
|
+
"global_override": False,
|
|
504
|
+
"declination_mode": None,
|
|
505
|
+
"apply_utm_convergence": False,
|
|
506
|
+
"override_lruds": False,
|
|
507
|
+
"lruds_at_to_station": False,
|
|
508
|
+
"apply_shot_flags": False,
|
|
509
|
+
"apply_total_exclusion": False,
|
|
510
|
+
"apply_plotting_exclusion": False,
|
|
511
|
+
"apply_length_exclusion": False,
|
|
512
|
+
"apply_close_exclusion": False,
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
while self._pos < len(self._data):
|
|
516
|
+
char = self._data[self._pos]
|
|
517
|
+
self._pos += 1
|
|
518
|
+
|
|
519
|
+
if char == ";":
|
|
520
|
+
result["raw_flags"] = self._data[start_pos : self._pos - 1]
|
|
521
|
+
return result
|
|
522
|
+
|
|
523
|
+
# Flag 1: G/g - Global override
|
|
524
|
+
if char == "G":
|
|
525
|
+
result["global_override"] = True
|
|
526
|
+
elif char == "g":
|
|
527
|
+
result["global_override"] = False
|
|
528
|
+
|
|
529
|
+
# Flag 2: I/E/A - Declination mode
|
|
530
|
+
elif char == "I":
|
|
531
|
+
result["declination_mode"] = "I"
|
|
532
|
+
elif char == "E":
|
|
533
|
+
result["declination_mode"] = "E"
|
|
534
|
+
elif char == "A":
|
|
535
|
+
result["declination_mode"] = "A"
|
|
536
|
+
|
|
537
|
+
# Flag 3: V/v - Apply UTM convergence
|
|
538
|
+
elif char == "V":
|
|
539
|
+
result["apply_utm_convergence"] = True
|
|
540
|
+
elif char == "v":
|
|
541
|
+
result["apply_utm_convergence"] = False
|
|
542
|
+
|
|
543
|
+
# Flag 4: O/o - Override LRUD associations
|
|
544
|
+
elif char == "O":
|
|
545
|
+
result["override_lruds"] = True
|
|
546
|
+
elif char == "o":
|
|
547
|
+
result["override_lruds"] = False
|
|
548
|
+
|
|
549
|
+
# Flag 5: T/t - LRUDs at To station
|
|
550
|
+
elif char == "T":
|
|
551
|
+
result["lruds_at_to_station"] = True
|
|
552
|
+
elif char == "t":
|
|
553
|
+
result["lruds_at_to_station"] = False
|
|
554
|
+
|
|
555
|
+
# Flag 6: S/s - Apply shot flags
|
|
556
|
+
elif char == "S":
|
|
557
|
+
result["apply_shot_flags"] = True
|
|
558
|
+
elif char == "s":
|
|
559
|
+
result["apply_shot_flags"] = False
|
|
560
|
+
|
|
561
|
+
# Flag 7: X/x - Apply total exclusion flags
|
|
562
|
+
elif char == "X":
|
|
563
|
+
result["apply_total_exclusion"] = True
|
|
564
|
+
elif char == "x":
|
|
565
|
+
result["apply_total_exclusion"] = False
|
|
566
|
+
|
|
567
|
+
# Flag 8: P/p - Apply plotting exclusion flags
|
|
568
|
+
elif char == "P":
|
|
569
|
+
result["apply_plotting_exclusion"] = True
|
|
570
|
+
elif char == "p":
|
|
571
|
+
result["apply_plotting_exclusion"] = False
|
|
572
|
+
|
|
573
|
+
# Flag 9: L/l - Apply length exclusion flags
|
|
574
|
+
elif char == "L":
|
|
575
|
+
result["apply_length_exclusion"] = True
|
|
576
|
+
elif char == "l":
|
|
577
|
+
result["apply_length_exclusion"] = False
|
|
578
|
+
|
|
579
|
+
# Flag 10: C/c - Apply close exclusion flags
|
|
580
|
+
elif char == "C":
|
|
581
|
+
result["apply_close_exclusion"] = True
|
|
582
|
+
elif char == "c":
|
|
583
|
+
result["apply_close_exclusion"] = False
|
|
584
|
+
|
|
585
|
+
# Silently skip unknown characters for lenient parsing
|
|
586
|
+
|
|
587
|
+
raise CompassParseException(
|
|
588
|
+
"missing or incomplete flags",
|
|
589
|
+
self._location(),
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
def _parse_folder_start_to_dict(self) -> dict[str, Any]:
|
|
593
|
+
"""Parse [FolderName; to dictionary."""
|
|
594
|
+
self._skip_whitespace()
|
|
595
|
+
# Read until semicolon
|
|
596
|
+
start = self._pos
|
|
597
|
+
while self._pos < len(self._data) and self._data[self._pos] != ";":
|
|
598
|
+
self._pos += 1
|
|
599
|
+
name = self._data[start : self._pos].strip()
|
|
600
|
+
self._expect(";")
|
|
601
|
+
return {"type": "folder_start", "name": name}
|
|
602
|
+
|
|
603
|
+
def _parse_folder_end_to_dict(self) -> dict[str, Any]:
|
|
604
|
+
"""Parse ]; to dictionary."""
|
|
605
|
+
self._skip_whitespace()
|
|
606
|
+
self._expect(";")
|
|
607
|
+
return {"type": "folder_end"}
|
|
608
|
+
|
|
609
|
+
def _parse_comment_to_dict(self) -> dict[str, Any]:
|
|
610
|
+
"""Parse / comment (to end of line) to dictionary."""
|
|
611
|
+
start = self._pos
|
|
612
|
+
self._skip_to_end_of_line()
|
|
613
|
+
comment = self._data[start : self._pos].strip()
|
|
614
|
+
return {"type": "comment", "comment": comment}
|
|
615
|
+
|
|
616
|
+
def _parse_unknown_directive_to_dict(self, directive_type: str) -> dict[str, Any]:
|
|
617
|
+
"""Parse unknown directive to semicolon for roundtrip fidelity."""
|
|
618
|
+
start = self._pos
|
|
619
|
+
|
|
620
|
+
while self._pos < len(self._data):
|
|
621
|
+
char = self._data[self._pos]
|
|
622
|
+
if char == ";":
|
|
623
|
+
content = self._data[start : self._pos]
|
|
624
|
+
self._pos += 1 # Skip the semicolon
|
|
625
|
+
return {
|
|
626
|
+
"type": "unknown",
|
|
627
|
+
"directive_type": directive_type,
|
|
628
|
+
"content": content,
|
|
629
|
+
}
|
|
630
|
+
self._pos += 1
|
|
631
|
+
|
|
632
|
+
# No semicolon found - use rest of data
|
|
633
|
+
content = self._data[start : self._pos]
|
|
634
|
+
return {
|
|
635
|
+
"type": "unknown",
|
|
636
|
+
"directive_type": directive_type,
|
|
637
|
+
"content": content,
|
|
638
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Survey module for parsing and formatting Compass .DAT files."""
|
|
3
|
+
|
|
4
|
+
from compass_lib.survey.format import format_dat_file
|
|
5
|
+
from compass_lib.survey.format import format_shot
|
|
6
|
+
from compass_lib.survey.format import format_trip
|
|
7
|
+
from compass_lib.survey.format import format_trip_header
|
|
8
|
+
from compass_lib.survey.models import CompassDatFile
|
|
9
|
+
from compass_lib.survey.models import CompassShot
|
|
10
|
+
from compass_lib.survey.models import CompassTrip
|
|
11
|
+
from compass_lib.survey.models import CompassTripHeader
|
|
12
|
+
from compass_lib.survey.parser import CompassSurveyParser
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"CompassDatFile",
|
|
16
|
+
"CompassShot",
|
|
17
|
+
"CompassSurveyParser",
|
|
18
|
+
"CompassTrip",
|
|
19
|
+
"CompassTripHeader",
|
|
20
|
+
"format_dat_file",
|
|
21
|
+
"format_shot",
|
|
22
|
+
"format_trip",
|
|
23
|
+
"format_trip_header",
|
|
24
|
+
]
|