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

166 statements  

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 

10 

11 

12class IAU_REPORT: # pylint: disable=invalid-name,too-few-public-methods 

13 """Version of the IAU report.""" 

14 

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 

18 

19 

20class ReferenceShape(Enum): 

21 """The different shapes of the celestial body.""" 

22 

23 SPHERE = "Sphere" 

24 ELLIPSE = "Ellipse" 

25 TRIAXIAL = "Triaxial" 

26 

27 

28class IBody(metaclass=ABCMeta): 

29 """Interface describing a celestial body.""" 

30 

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 ) 

44 

45 @abstractproperty # pylint: disable=bad-option-value,deprecated-decorator 

46 def name(self) -> str: 

47 """Returns the name of the shape. 

48 

49 Raises: 

50 NotImplementedError: Not implemented 

51 

52 Returns: 

53 str: the name of the shape 

54 """ 

55 raise NotImplementedError("Not implemented") 

56 

57 @abstractproperty # pylint: disable=bad-option-value,deprecated-decorator 

58 def shape(self) -> ReferenceShape: 

59 """Returns the name of the shape. 

60 

61 Raises: 

62 NotImplementedError: Not implemented 

63 

64 Returns: 

65 ReferenceShape: the name of the shape 

66 """ 

67 raise NotImplementedError("Not implemented") 

68 

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. 

72 

73 Raises: 

74 NotImplementedError: Not implemented 

75 

76 Returns: 

77 str: the warning during the creation of the shape 

78 """ 

79 raise NotImplementedError("Not implemented") 

80 

81 @warning.setter 

82 def warning(self, value: Optional[str]): 

83 raise NotImplementedError("Not implemented") 

84 

85 @abstractmethod 

86 def wkt(self) -> str: 

87 """Returns the WKT of the shape. 

88 

89 Raises: 

90 NotImplementedError: Not implemented 

91 

92 Returns: 

93 str: the WKT of the shape 

94 """ 

95 raise NotImplementedError("Not implemented") 

96 

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. 

107 

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 

115 

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 

123 

124 Raises: 

125 ValueError: Unsupported shape 

126 

127 Returns: 

128 IBody: A celectial body 

129 """ 

130 mean_radius: float 

131 warning: Optional[str] = None 

132 

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 

174 

175 

176@IBody.register 

177class Ellipsoid(IBody): 

178 """An Ellipoid shape.""" 

179 

180 TEMPLATE = """ELLIPSOID["$ellipsoide_name ($version)", $radius, $inverse_flat, 

181\t\tLENGTHUNIT["metre", 1, ID["EPSG", 9001]]]""" 

182 

183 def __init__(self, name: str, radius: float, inverse_flat: float): 

184 """Create an ellipsoid shape. 

185 

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 

195 

196 @property 

197 def name(self) -> str: 

198 """The body name. 

199 

200 :getter: Returns the body name 

201 :type: str 

202 """ 

203 return self.__name 

204 

205 @property 

206 def radius(self) -> float: 

207 """The radius in meter. 

208 

209 :getter: Returns the radius 

210 :type: float 

211 """ 

212 return self.__radius 

213 

214 @property 

215 def inverse_flat(self) -> float: 

216 """The inverse flatenning. 

217 

218 :getter: Returns the inverse flatenning 

219 :type: float 

220 """ 

221 return self.__inverse_flat 

222 

223 @property 

224 def shape(self) -> ReferenceShape: 

225 """The shape. 

226 

227 :getter: Returns the shape 

228 :type: ReferenceShape 

229 """ 

230 return ReferenceShape.ELLIPSE 

231 

232 @property 

233 def warning(self) -> Optional[str]: 

234 """The warning related to the body description creation. 

235 

236 :getter: Returns the warning 

237 :setter: the warning value 

238 :type: Optional[str] 

239 """ 

240 return self.__warning 

241 

242 @warning.setter 

243 def warning(self, value: Optional[str]): 

244 self.__warning = value 

245 

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 

255 

256 def wkt(self) -> str: 

257 """Returns the WKT of the ellipsoidal body. 

258 

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 

270 

271 

272@IBody.register 

273class Sphere(IBody): 

274 """A sphere shape.""" 

275 

276 TEMPLATE = """ELLIPSOID["$ellipsoide_name ($version) - Sphere", $radius, 0, 

277\t\tLENGTHUNIT["metre", 1, ID["EPSG", 9001]]]""" 

278 

279 def __init__(self, name: str, radius: float): 

280 """Create a sphere desctription 

281 

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 

289 

290 @property 

291 def name(self) -> str: 

292 """The body name. 

293 

294 :getter: Returns the body name 

295 :type: str 

296 """ 

297 return self.__name 

298 

299 @property 

300 def radius(self) -> float: 

301 """The radius in meter. 

302 

303 :getter: Returns the radius in meter 

304 :type: float 

305 """ 

306 return self.__radius 

307 

308 @property 

309 def shape(self) -> ReferenceShape: 

310 """The shape. 

311 

312 :getter: Returns the shape 

313 :type: ReferenceShape 

314 """ 

315 return ReferenceShape.SPHERE 

316 

317 @property 

318 def warning(self) -> Optional[str]: 

319 """The warning related to the body description creation. 

320 

321 :getter: Returns the warning 

322 :setter: the warning value 

323 :type: Optional[str] 

324 """ 

325 return self.__warning 

326 

327 @warning.setter 

328 def warning(self, value: Optional[str]): 

329 self.__warning = value 

330 

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 

340 

341 def wkt(self) -> str: 

342 """Returns the WKT of the spherical body. 

343 

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 

354 

355 

356@IBody.register 

357class Triaxial(IBody): 

358 """A triaxial shape.""" 

359 

360 TEMPLATE = """TRIAXIAL["$ellipsoide_name ($version)", $semi_major, $semi_median, $semi_minor, 

361\t\tLENGTHUNIT["metre", 1, ID["EPSG", 9001]]]""" 

362 

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. 

371 

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 

383 

384 @property 

385 def name(self) -> str: 

386 """The body name. 

387 

388 :getter: Returns the body name 

389 :type: str 

390 """ 

391 return self.__name 

392 

393 @property 

394 def semi_major(self) -> float: 

395 """The semi major axis. 

396 

397 :getter: Returns the length of the semi major axis 

398 :type: float 

399 """ 

400 return self.__semi_major 

401 

402 @property 

403 def semi_minor(self) -> float: 

404 """The semi minor axis. 

405 

406 :getter: Returns the length of the semi minor axis 

407 :type: float 

408 """ 

409 return self.__semi_minor 

410 

411 @property 

412 def semi_median(self) -> float: 

413 """The semi median. 

414 

415 :getter: Returns the length of the semi median axis 

416 :type: float 

417 """ 

418 return self.__semi_median 

419 

420 @property 

421 def shape(self) -> ReferenceShape: 

422 """The shape. 

423 

424 :getter: Returns the shape 

425 :type: ReferenceShape 

426 """ 

427 return ReferenceShape.TRIAXIAL 

428 

429 @property 

430 def warning(self) -> Optional[str]: 

431 """The warning related to the body description creation. 

432 

433 :getter: Returns the warning 

434 :setter: the warning value 

435 :type: Optional[str] 

436 """ 

437 return self.__warning 

438 

439 @warning.setter 

440 def warning(self, value: Optional[str]): 

441 self.__warning = value 

442 

443 def wkt(self) -> str: 

444 """Returns the WKT of the triaxial body. 

445 

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