Coverage for csvforwkt/body.py: 92%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""This module is reponsible to handle a celestial body."""
3from abc import ABCMeta
4from abc import abstractmethod
5from abc import abstractproperty
6from enum import Enum
7from string import Template
8from typing import Optional
9from typing import Union
12class IAU_REPORT: # pylint: disable=invalid-name,too-few-public-methods
13 """Version of the IAU report."""
15 VERSION: str = "2015"
16 DOI_IAU: str = "https://doi.org/10.1007/s10569-017-9805-5"
17 SOURCE_IAU: str = "Source of IAU Coordinate systems: " + DOI_IAU
20class ReferenceShape(Enum):
21 """The different shapes of the celestial body."""
23 SPHERE = "Sphere"
24 ELLIPSE = "Ellipse"
25 TRIAXIAL = "Triaxial"
28class IBody(metaclass=ABCMeta):
29 """Interface describing a celestial body."""
31 @classmethod
32 def __subclasshook__(cls, subclass):
33 return (
34 hasattr(subclass, "wkt")
35 and callable(subclass.wkt)
36 and hasattr(subclass, "name")
37 and callable(subclass.name)
38 and hasattr(subclass, "shape")
39 and callable(subclass.shape)
40 and hasattr(subclass, "warning")
41 and callable(subclass.warning)
42 or NotImplemented
43 )
45 @abstractproperty # pylint: disable=bad-option-value,deprecated-decorator
46 def name(self) -> str:
47 """Returns the name of the shape.
49 Raises:
50 NotImplementedError: Not implemented
52 Returns:
53 str: the name of the shape
54 """
55 raise NotImplementedError("Not implemented")
57 @abstractproperty # pylint: disable=bad-option-value,deprecated-decorator
58 def shape(self) -> ReferenceShape:
59 """Returns the name of the shape.
61 Raises:
62 NotImplementedError: Not implemented
64 Returns:
65 ReferenceShape: the name of the shape
66 """
67 raise NotImplementedError("Not implemented")
69 @abstractproperty # pylint: disable=bad-option-value,deprecated-decorator
70 def warning(self) -> Optional[str]:
71 """Returns the warning during the creation of the shape.
73 Raises:
74 NotImplementedError: Not implemented
76 Returns:
77 str: the warning during the creation of the shape
78 """
79 raise NotImplementedError("Not implemented")
81 @warning.setter
82 def warning(self, value: Optional[str]):
83 raise NotImplementedError("Not implemented")
85 @abstractmethod
86 def wkt(self) -> str:
87 """Returns the WKT of the shape.
89 Raises:
90 NotImplementedError: Not implemented
92 Returns:
93 str: the WKT of the shape
94 """
95 raise NotImplementedError("Not implemented")
97 @staticmethod
98 def create( # pylint: disable=invalid-name,too-few-public-methods,too-many-arguments
99 shape: ReferenceShape,
100 name: str,
101 semi_major: float,
102 semi_minor: float,
103 axisb: float,
104 mean: float,
105 ) -> "IBody":
106 """Create a shape.
108 Rules to compute the sphere radius:
109 * mean = -1 => Use R_m = (a+b+c)/3 as mean radius
110 * semi major = -1 or semi minor = -1 => Use mean radius as sphere radius
111 * semi major < semi minor => Use mean radius as sphere radius
112 * triaxial bodies => Use mean radius as sphere radius
113 * semi major = semi minor = axis b => Use mean radius
114 * biaxial bodies => Use semi major as sphere radius
116 Args:
117 shape (ReferenceShape): type of shape
118 name (str): name of the shape
119 semi_major (float): semi major axis in meter
120 semi_minor (float): semi minor axis in meter
121 axisb (float): third axis in meter
122 mean (float): mean radius in meter
124 Raises:
125 ValueError: Unsupported shape
127 Returns:
128 IBody: A celectial body
129 """
130 mean_radius: float
131 warning: Optional[str] = None
133 result: IBody
134 if shape == ReferenceShape.SPHERE:
135 if mean == -1:
136 mean_radius = (semi_major + semi_minor + axisb) / 3
137 warning = "Use R_m = (a+b+c)/3 as mean radius. Use mean radius as sphere radius for interoperability. "
138 elif semi_major == -1 or semi_minor == -1:
139 mean_radius = mean
140 warning = (
141 "Use mean radius as sphere radius for interoperability. "
142 )
143 elif semi_major < semi_minor: # Hartley case
144 mean_radius = mean
145 warning = (
146 "Use mean radius as sphere radius for interoperability. "
147 )
148 elif axisb not in (semi_major, semi_minor): # Triaxial case
149 mean_radius = mean
150 warning = (
151 "Use mean radius as sphere radius for interoperability. "
152 )
153 elif (
154 semi_major == axisb and semi_minor == axisb
155 ): # Sun or moon case (No approximation)
156 mean_radius = mean
157 else:
158 mean_radius = semi_major # Biaxial case
159 warning = "Use semi-major radius as sphere radius for interoperability. "
160 result = Sphere(name, mean_radius)
161 elif shape == ReferenceShape.ELLIPSE:
162 inverse_flat: float
163 if semi_major == semi_minor:
164 inverse_flat = 0
165 else:
166 inverse_flat = semi_major / (semi_major - semi_minor)
167 result = Ellipsoid(name, semi_major, inverse_flat)
168 elif shape == ReferenceShape.TRIAXIAL:
169 result = Triaxial(name, semi_major, axisb, semi_minor)
170 else:
171 raise ValueError(f"Unsuported shape: {shape}")
172 result.warning = warning
173 return result
176@IBody.register
177class Ellipsoid(IBody):
178 """An Ellipoid shape."""
180 TEMPLATE = """ELLIPSOID["$ellipsoide_name ($version)", $radius, $inverse_flat,
181\t\tLENGTHUNIT["metre", 1, ID["EPSG", 9001]]]"""
183 def __init__(self, name: str, radius: float, inverse_flat: float):
184 """Create an ellipsoid shape.
186 Args:
187 name (str): name of the shape
188 radius (float): radius of the shape in meter
189 inverse_flat (float): inverse flatenning of the shape in meter
190 """
191 self.__name: str = name
192 self.__radius: float = radius
193 self.__inverse_flat: float = inverse_flat
194 self.__warning: Optional[str] = None
196 @property
197 def name(self) -> str:
198 """The body name.
200 :getter: Returns the body name
201 :type: str
202 """
203 return self.__name
205 @property
206 def radius(self) -> float:
207 """The radius in meter.
209 :getter: Returns the radius
210 :type: float
211 """
212 return self.__radius
214 @property
215 def inverse_flat(self) -> float:
216 """The inverse flatenning.
218 :getter: Returns the inverse flatenning
219 :type: float
220 """
221 return self.__inverse_flat
223 @property
224 def shape(self) -> ReferenceShape:
225 """The shape.
227 :getter: Returns the shape
228 :type: ReferenceShape
229 """
230 return ReferenceShape.ELLIPSE
232 @property
233 def warning(self) -> Optional[str]:
234 """The warning related to the body description creation.
236 :getter: Returns the warning
237 :setter: the warning value
238 :type: Optional[str]
239 """
240 return self.__warning
242 @warning.setter
243 def warning(self, value: Optional[str]):
244 self.__warning = value
246 def _convert( # pylint: disable=no-self-use
247 self, number: float
248 ) -> Union[int, float]:
249 result: Union[int, float]
250 if abs(number - round(number, 0)) <= 1e-10:
251 result = int(number)
252 else:
253 result = number
254 return result
256 def wkt(self) -> str:
257 """Returns the WKT of the ellipsoidal body.
259 Returns:
260 str: the WKT of the shape
261 """
262 datum_template: Template = Template(Ellipsoid.TEMPLATE)
263 datum = datum_template.substitute(
264 version=IAU_REPORT.VERSION,
265 ellipsoide_name=self.name,
266 radius=self._convert(self.radius),
267 inverse_flat=self.inverse_flat,
268 )
269 return datum
272@IBody.register
273class Sphere(IBody):
274 """A sphere shape."""
276 TEMPLATE = """ELLIPSOID["$ellipsoide_name ($version) - Sphere", $radius, 0,
277\t\tLENGTHUNIT["metre", 1, ID["EPSG", 9001]]]"""
279 def __init__(self, name: str, radius: float):
280 """Create a sphere desctription
282 Args:
283 name (str): body name
284 radius (float): radius in meter of the sphere
285 """
286 self.__name: str = name
287 self.__radius: float = radius
288 self.__warning: Optional[str] = None
290 @property
291 def name(self) -> str:
292 """The body name.
294 :getter: Returns the body name
295 :type: str
296 """
297 return self.__name
299 @property
300 def radius(self) -> float:
301 """The radius in meter.
303 :getter: Returns the radius in meter
304 :type: float
305 """
306 return self.__radius
308 @property
309 def shape(self) -> ReferenceShape:
310 """The shape.
312 :getter: Returns the shape
313 :type: ReferenceShape
314 """
315 return ReferenceShape.SPHERE
317 @property
318 def warning(self) -> Optional[str]:
319 """The warning related to the body description creation.
321 :getter: Returns the warning
322 :setter: the warning value
323 :type: Optional[str]
324 """
325 return self.__warning
327 @warning.setter
328 def warning(self, value: Optional[str]):
329 self.__warning = value
331 def _convert( # pylint: disable=no-self-use
332 self, number: float
333 ) -> Union[int, float]:
334 result: Union[int, float]
335 if abs(number - round(number, 0)) <= 1e-10:
336 result = int(number)
337 else:
338 result = number
339 return result
341 def wkt(self) -> str:
342 """Returns the WKT of the spherical body.
344 Returns:
345 str: the WKT of the shape
346 """
347 datum_template: Template = Template(Sphere.TEMPLATE)
348 datum = datum_template.substitute(
349 ellipsoide_name=self.name,
350 version=IAU_REPORT.VERSION,
351 radius=self._convert(self.radius),
352 )
353 return datum
356@IBody.register
357class Triaxial(IBody):
358 """A triaxial shape."""
360 TEMPLATE = """TRIAXIAL["$ellipsoide_name ($version)", $semi_major, $semi_median, $semi_minor,
361\t\tLENGTHUNIT["metre", 1, ID["EPSG", 9001]]]"""
363 def __init__(
364 self,
365 name: str,
366 semi_major: float,
367 semi_median: float,
368 semi_minor: float,
369 ):
370 """Create a triaxial shape.
372 Args:
373 name (str): body name
374 semi_major (float): axis one
375 semi_median (float): axis two
376 semi_minor (float): axis third
377 """
378 self.__name: str = name
379 self.__semi_major: float = semi_major
380 self.__semi_minor: float = semi_minor
381 self.__semi_median: float = semi_median
382 self.__warning: Optional[str] = None
384 @property
385 def name(self) -> str:
386 """The body name.
388 :getter: Returns the body name
389 :type: str
390 """
391 return self.__name
393 @property
394 def semi_major(self) -> float:
395 """The semi major axis.
397 :getter: Returns the length of the semi major axis
398 :type: float
399 """
400 return self.__semi_major
402 @property
403 def semi_minor(self) -> float:
404 """The semi minor axis.
406 :getter: Returns the length of the semi minor axis
407 :type: float
408 """
409 return self.__semi_minor
411 @property
412 def semi_median(self) -> float:
413 """The semi median.
415 :getter: Returns the length of the semi median axis
416 :type: float
417 """
418 return self.__semi_median
420 @property
421 def shape(self) -> ReferenceShape:
422 """The shape.
424 :getter: Returns the shape
425 :type: ReferenceShape
426 """
427 return ReferenceShape.TRIAXIAL
429 @property
430 def warning(self) -> Optional[str]:
431 """The warning related to the body description creation.
433 :getter: Returns the warning
434 :setter: the warning value
435 :type: Optional[str]
436 """
437 return self.__warning
439 @warning.setter
440 def warning(self, value: Optional[str]):
441 self.__warning = value
443 def wkt(self) -> str:
444 """Returns the WKT of the triaxial body.
446 Returns:
447 str: the WKT of the shape
448 """
449 datum_template: Template = Template(Triaxial.TEMPLATE)
450 datum = datum_template.substitute(
451 ellipsoide_name=self.name,
452 version=IAU_REPORT.VERSION,
453 semi_major=self.semi_major,
454 semi_median=self.semi_median,
455 semi_minor=self.semi_minor,
456 )
457 return datum