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
@@ -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 reverse(cls, name):
6
- return cls._value2member_map_[name]
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
- # ============================== Azimuth ============================== #
70
+ class FileExtension(str, Enum):
71
+ """File extensions for various file formats (with dot).
9
72
 
10
- # export const azimuthUnits: { [string]: DisplayAzimuthUnit } = {
11
- # D: 'degrees',
12
- # Q: 'quads',
13
- # G: 'gradians',
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
- GRADIANS = "G"
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
- # export const inclinationUnits: { [string]: DisplayInclinationUnit } = {
24
- # D: 'degrees',
25
- # G: 'percentGrade',
26
- # M: 'degreesAndMinutes',
27
- # R: 'gradians',
28
- # W: 'depthGauge',
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
- GRADIANS = "R"
145
+ GRADS = "R"
36
146
  DEPTH_GAUGE = "W"
37
147
 
38
- # ============================== Length Unit ============================== #
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
- # export const lengthUnits: { [string]: DisplayLengthUnit } = {
42
- # D: 'decimalFeet',
43
- # I: 'feetAndInches',
44
- # M: 'meters',
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
- # ============================== LRUD ============================== #
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
- # export const lrudItems: { [string]: LrudItem } = {
55
- # L: 'left',
56
- # R: 'right',
57
- # U: 'up',
58
- # D: 'down',
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
- # export const shotMeasurementItems: { [string]: ShotMeasurementItem } = {
70
- # L: 'length',
71
- # A: 'frontsightAzimuth',
72
- # D: 'frontsightInclination',
73
- # a: 'backsightAzimuth',
74
- # d: 'backsightInclination',
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
- # export const stationSides: { [string]: StationSide } = {
87
- # F: 'from',
88
- # T: 'to',
89
- # }
245
+ class Severity(str, Enum):
246
+ """Severity level for parse errors.
90
247
 
91
- class StationSide(CustomEnum):
92
- FROM = "F"
93
- TO = "T"
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 ShotFlag(CustomEnum):
99
- EXCLUDE_PLOTING = "P"
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
- __start_token__ = r"#\|"
106
- __end_token__ = r"#"
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
- if __name__ == "__main__":
111
- print(StationSide.FROM.value)
112
- print(StationSide.reverse("F"))
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
+ )
@@ -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)