Coverage for csvforwkt/crs.py: 94%

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

239 statements  

1# -*- coding: utf-8 -*- 

2"""One body can be defined by one of the two following shapes: 

3 * a biaxial body 

4 * a triaxial body 

5 

6 

7At each shape, we can define three reference frames : 

8 * planetocentric frame with a sphere for interoperability purpose 

9 * planetocentric frame 

10 * planetographic frame 

11 

12For a spherical shape, the planetocentric latitude and the planetographic 

13latitude are identical. So a planetographic latitude is used. 

14 

15Planetographic longitude is usually defined such that the sub-observer 

16longitude increases with time as seen by a distant, fixed observer 

17 

18Positive logitudes in one direction are defined with the following rule 

19 

20.. uml:: 

21 :caption: Positive longitude rules 

22 

23 start 

24 

25 if (historical reason or IAU_code >= 9000?) then (yes) 

26 :East; 

27 stop 

28 else (no ) 

29 if (ocentric frame ?) then (yes) 

30 :East; 

31 stop 

32 else (no) 

33 if (rotation == "Direct" ?) then (yes) 

34 :West; 

35 stop 

36 else (no) 

37 :East; 

38 stop 

39 endif 

40 endif 

41 endif 

42 

43""" 

44# pylint: disable=too-many-lines 

45from abc import ABCMeta 

46from abc import abstractmethod 

47from abc import abstractproperty 

48from enum import Enum 

49from string import Template 

50from typing import cast 

51from typing import Dict 

52from typing import Generator 

53from typing import List 

54from typing import Tuple 

55 

56import numpy as np # pylint: disable=import-error 

57import pandas as pd # pylint: disable=import-error 

58 

59from .body import IAU_REPORT 

60from .body import IBody 

61from .body import ReferenceShape 

62from .datum import Anchor 

63from .datum import Datum 

64 

65 

66class ICrs(metaclass=ABCMeta): 

67 """High level class that handles a Coordinate Reference System.""" 

68 

69 @classmethod 

70 def __subclasshook__(cls, subclass): 

71 return ( 

72 hasattr(subclass, "iau_code") 

73 and callable(subclass.iau_code) 

74 and hasattr(subclass, "wkt") 

75 and callable(subclass.wkt) 

76 and hasattr(subclass, "datum") 

77 and callable(subclass.datum) 

78 or NotImplemented 

79 ) 

80 

81 @abstractproperty # pylint: disable=no-self-use,bad-option-value,deprecated-decorator 

82 def iau_code(self) -> int: 

83 """IAU code. 

84 

85 :getter: Returns the IAU code 

86 :type: int 

87 """ 

88 raise NotImplementedError("Not implemented") 

89 

90 @abstractproperty # pylint: disable=no-self-use,bad-option-value,deprecated-decorator 

91 def datum(self) -> Datum: 

92 """Datum. 

93 

94 :getter: Returns the datum 

95 :type: Datum 

96 """ 

97 raise NotImplementedError("Not implemented") 

98 

99 @abstractmethod 

100 def wkt(self) -> str: 

101 """Returns the WKT description. 

102 

103 :getter: Returns the WKT description 

104 :type: str 

105 """ 

106 raise NotImplementedError("Not implemented") 

107 

108 

109class CrsType(Enum): 

110 """Type of CRS.""" 

111 

112 OCENTRIC = "Ocentric" 

113 OGRAPHIC = "Ographic" 

114 

115 

116class BodyCrsCode(Enum): 

117 """Code related to the shape and the coordinate reference system.""" 

118 

119 SPHERE_OCENTRIC = ("Sphere", CrsType.OCENTRIC.value, 0) 

120 ELLIPSE_OGRAPHIC = ("Ellipse", CrsType.OGRAPHIC.value, 1) 

121 ELLIPSE_OCENTRIC = ("Ellipse", CrsType.OCENTRIC.value, 2) 

122 TRIAXIAL_OGRAPHIC = ("Triaxial", CrsType.OGRAPHIC.value, 3) 

123 TRIAXIAL_OCENTRIC = ("Triaxial", CrsType.OCENTRIC.value, 4) 

124 

125 def __init__(self, shape: str, reference: str, code: int): 

126 """Creates the enum 

127 

128 Args: 

129 shape (str): shape 

130 reference (str): referential 

131 code (int): code of (shape, referential) 

132 """ 

133 self.shape: str = shape 

134 self.reference: str = reference 

135 self.code = code 

136 

137 def get_code(self, naif_code: int) -> int: 

138 """Compute the code of the body based on the Naif code. 

139 

140 Args: 

141 naif_code (int): naif code 

142 

143 Returns: 

144 int: the code of the body 

145 """ 

146 return naif_code * 100 + self.code 

147 

148 

149@ICrs.register 

150class BodyCrs(ICrs): 

151 """The description of the body Coordinate Reference System.""" 

152 

153 TEMPLATE_OGRAPHIC = """GEOGCRS["$name ($version) / Ographic", 

154 $datum, 

155\tCS[ellipsoidal, 2], 

156\t AXIS["geodetic latitude (Lat)", north, 

157\t ORDER[1], 

158\t ANGLEUNIT["degree", 0.0174532925199433]], 

159\t AXIS["geodetic longitude (Lon)", $direction, 

160\t ORDER[2], 

161\t ANGLEUNIT["degree", 0.0174532925199433]], 

162\tID["IAU", $number, $version], 

163\tREMARK["$remark"]]""" 

164 

165 TEMPLATE_OCENTRIC = """GEODCRS["$name ($version) / Ocentric", 

166 $datum, 

167\tCS[spherical, 2], 

168\t AXIS["planetocentric latitude (U)", north, 

169\t ORDER[1], 

170\t ANGLEUNIT["degree", 0.0174532925199433]], 

171\t AXIS["planetocentric longitude (V)", east, 

172\t ORDER[2], 

173\t ANGLEUNIT["degree", 0.0174532925199433]], 

174\tID["IAU", $number, $version], 

175\tREMARK["$remark"]]""" 

176 

177 TEMPLATE_SPHERE = """GEOGCRS["$name ($version) - Sphere / Ocentric", 

178 $datum, 

179\tCS[ellipsoidal, 2], 

180\t AXIS["geodetic latitude (Lat)", north, 

181\t ORDER[1], 

182\t ANGLEUNIT["degree", 0.0174532925199433]], 

183\t AXIS["geodetic longitude (Lon)", east, 

184\t ORDER[2], 

185\t ANGLEUNIT["degree", 0.0174532925199433]], 

186\tID["IAU", $number, $version], 

187\tREMARK["$remark"]]""" 

188 

189 def __init__( 

190 self, datum: Datum, number_body: int, direction: str, crs_type: CrsType 

191 ): 

192 """Create a Coordinate Reference System for a celestial body 

193 

194 Args: 

195 datum (Datum): datum of the body 

196 number_body (int): IAU code 

197 direction (str): rotation sens of the body 

198 crs_type (CrsType): type of CRS 

199 """ 

200 self.__datum: Datum = datum 

201 self.__crs_type: CrsType = crs_type 

202 self.__direction: str = self._create_direction( 

203 datum.name, direction, crs_type, number_body 

204 ) 

205 self.__name: str = datum.name 

206 

207 template, number = self._get_template_and_number(number_body) 

208 self.__number: int = number 

209 self.__template: str = template 

210 

211 def _get_template_and_number( # pylint: disable=no-self-use 

212 self, naif_code: int 

213 ) -> Tuple[str, int]: 

214 """Returns the template and the number 

215 

216 Args: 

217 naif_code (int): naif code 

218 

219 Raises: 

220 ValueError: "Unknown shape or CRS 

221 

222 Returns: 

223 Tuple[str, int]: template and number 

224 """ 

225 template: str 

226 number: int 

227 

228 if self.crs_type == CrsType.OCENTRIC: 

229 if self.datum.body.shape == ReferenceShape.SPHERE: 

230 template = BodyCrs.TEMPLATE_SPHERE 

231 number = BodyCrsCode.SPHERE_OCENTRIC.get_code(naif_code) 

232 elif self.datum.body.shape == ReferenceShape.ELLIPSE: 

233 template = BodyCrs.TEMPLATE_OCENTRIC 

234 number = BodyCrsCode.ELLIPSE_OCENTRIC.get_code(naif_code) 

235 elif self.datum.body.shape == ReferenceShape.TRIAXIAL: 

236 template = BodyCrs.TEMPLATE_OCENTRIC 

237 number = BodyCrsCode.TRIAXIAL_OCENTRIC.get_code(naif_code) 

238 else: 

239 raise ValueError( 

240 f"Unknown shape : {self.datum.body.shape} for {self.crs_type}" 

241 ) 

242 elif self.crs_type == CrsType.OGRAPHIC: 

243 template = BodyCrs.TEMPLATE_OGRAPHIC 

244 if self.datum.body.shape == ReferenceShape.ELLIPSE: 

245 number = BodyCrsCode.ELLIPSE_OGRAPHIC.get_code(naif_code) 

246 elif self.datum.body.shape == ReferenceShape.TRIAXIAL: 

247 number = BodyCrsCode.TRIAXIAL_OGRAPHIC.get_code(naif_code) 

248 else: 

249 raise ValueError( 

250 f"Unknown shape : {self.datum.body.shape} for {self.crs_type}" 

251 ) 

252 else: 

253 raise ValueError(f"Unknown CRS : {self.crs_type}") 

254 

255 return template, number 

256 

257 def _create_direction( 

258 self, 

259 name: str, 

260 rotation: str, 

261 crs_type: CrsType, 

262 number_body: int, 

263 ): # pylint: disable=no-self-use 

264 """Returns the direction sens according to the rotation. 

265 

266 Args: 

267 name (str): body name 

268 rotation (str): rotation of the body 

269 crs_type (CrsType): Type of CRS 

270 number_body (int): IAU code of the body 

271 

272 

273 Returns: 

274 str: _description_ 

275 """ 

276 direction: str 

277 # longitude ographic is always to East for small bodies, comets, dwarf planets 

278 # historical reason for SUN, EARTH and MOON 

279 if number_body >= 90000 or name.upper() in ["SUN", "EARTH", "MOON"]: 

280 direction = "east" 

281 

282 # always to east in ocentric 

283 elif crs_type == CrsType.OCENTRIC: 

284 direction = "east" 

285 

286 # when Direct => West 

287 elif rotation == "Direct": 

288 direction = "west" 

289 

290 # last case : east 

291 else: 

292 direction = "east" 

293 return direction 

294 

295 def _create_remark(self) -> str: 

296 """Returns the content of the remark. 

297 

298 Returns: 

299 str: the content of the remark 

300 """ 

301 result: str 

302 if self.datum.body.warning is None: 

303 result = IAU_REPORT.SOURCE_IAU 

304 else: 

305 result = self.datum.body.warning + IAU_REPORT.SOURCE_IAU 

306 return result 

307 

308 @property 

309 def crs_type(self) -> CrsType: 

310 """Returns the type of CRS. 

311 

312 Returns: 

313 CrsType: Type of CRS 

314 """ 

315 return self.__crs_type 

316 

317 @property 

318 def name(self) -> str: 

319 """Returns the name of the body. 

320 

321 Returns: 

322 str: the name of the body 

323 """ 

324 return self.__name 

325 

326 @property 

327 def datum(self) -> Datum: 

328 """Returns the datum of the body. 

329 

330 Returns: 

331 Datum: the datum of the body 

332 """ 

333 return self.__datum 

334 

335 @property 

336 def direction(self) -> str: 

337 """Returns the direction where the longitude is counted positively. 

338 

339 Returns: 

340 str: the direction 

341 """ 

342 return self.__direction 

343 

344 @property 

345 def iau_code(self) -> int: 

346 """Returns the IAU code. 

347 

348 Returns: 

349 str: the IAU code 

350 """ 

351 return self.__number 

352 

353 def wkt(self) -> str: 

354 """Returns the WKT of the celestial body. 

355 

356 Returns: 

357 str: the WKT of the celestial body 

358 """ 

359 biaxialbody_template = Template(self.__template) 

360 datum = biaxialbody_template.substitute( 

361 name=self.name, 

362 version=IAU_REPORT.VERSION, 

363 datum=self.datum.wkt(), 

364 number=self.iau_code, 

365 direction=self.direction, 

366 remark=self._create_remark(), 

367 ) 

368 return datum 

369 

370 

371class Planetocentric: 

372 """Computes the planetocentric coordinate reference system.""" 

373 

374 def __init__(self, row: pd.Series, ref_shape: ReferenceShape): 

375 """Creates a description of a planetocentric Coordinate Reference 

376 System. 

377 

378 Args: 

379 row (pd.Series): description of the current body 

380 ref_shape(ReferenceShape) : Reference of the shape 

381 

382 Returns: 

383 ICrs: Coordinate Reference System description 

384 """ 

385 self.__row: pd.dataframe = row 

386 self.__ref_shape: ReferenceShape = ref_shape 

387 self.__crs: BodyCrs = self._crs() 

388 

389 @property 

390 def row(self) -> pd.DataFrame: 

391 """Description of the current body. 

392 

393 Returns: 

394 str: Description of the current body 

395 """ 

396 return self.__row 

397 

398 @property 

399 def ref_shape(self) -> ReferenceShape: 

400 """Shape related to this coordinate reference system. 

401 

402 Returns: 

403 ReferenceShape: type of shape 

404 """ 

405 return self.__ref_shape 

406 

407 @property 

408 def crs_type(self) -> CrsType: 

409 """Type of coordinate reference system. 

410 

411 Returns: 

412 CrsType: type of coordinate reference system 

413 """ 

414 return CrsType.OCENTRIC 

415 

416 @property 

417 def crs(self) -> BodyCrs: 

418 """Coordinate reference system of the body. 

419 

420 Returns: 

421 BodyCrs: coordinate reference system of the body 

422 """ 

423 return self.__crs 

424 

425 def _create_body(self) -> IBody: 

426 """Creates the description of the coordinate reference system for the 

427 body. 

428 

429 Returns: 

430 IBody: the coordinate reference system for the body 

431 """ 

432 return IBody.create( 

433 self.ref_shape, 

434 self.row["Body"], 

435 self.row["IAU2015_Semimajor"], 

436 self.row["IAU2015_Semiminor"], 

437 self.row["IAU2015_Axisb"], 

438 self.row["IAU2015_Mean"], 

439 ) 

440 

441 def _create_datum(self, body: IBody) -> Datum: 

442 """Creates the description of the datum related to the body. 

443 

444 Args: 

445 body (IBody): the body 

446 

447 Returns: 

448 Datum: the datum related to the body 

449 """ 

450 anchor: Anchor = Anchor( 

451 f"{self.row['origin_long_name']} : {self.row['origin_lon_pos']}" 

452 ) 

453 return Datum.create(self.row["Body"], body, anchor) 

454 

455 def _create_crs(self, datum: Datum) -> BodyCrs: 

456 """Creates a description of the planetocentric reference system based 

457 on the datum. 

458 

459 Args: 

460 datum (Datum): datum of the body 

461 

462 Returns: 

463 BodyCrs: the planetocentric reference system based on the datum 

464 """ 

465 return BodyCrs( 

466 datum, 

467 self.row["Naif_id"], 

468 self.row["rotation"], 

469 CrsType.OCENTRIC, 

470 ) 

471 

472 def _crs(self) -> BodyCrs: 

473 """Creates a description of a planetocentric Coordinate Reference 

474 System. 

475 

476 Returns: 

477 BodyCrs: Coordinate Reference System description of the body 

478 """ 

479 shape: IBody = self._create_body() 

480 datum: Datum = self._create_datum(shape) 

481 crs: BodyCrs = self._create_crs(datum) 

482 return crs 

483 

484 def wkt(self) -> str: 

485 """Returns the WKT of the coordinate reference system. 

486 

487 Returns: 

488 str: the WKT of the coordinate reference system 

489 """ 

490 return self.__crs.wkt() 

491 

492 

493class Planetographic(Planetocentric): 

494 """Computes the planetographic coordinate reference system.""" 

495 

496 @property 

497 def crs_type(self) -> CrsType: 

498 """Type of coordinate reference system. 

499 

500 Returns: 

501 CrsType: type of coordinate reference system 

502 """ 

503 return CrsType.OGRAPHIC 

504 

505 def _create_crs(self, datum: Datum) -> BodyCrs: 

506 """Creates a planetographic coordinate reference system based on a 

507 datum. 

508 

509 Args: 

510 datum (Datum): datum of the body 

511 

512 Returns: 

513 BodyCrs: planetographic coordinate reference system 

514 """ 

515 return BodyCrs( 

516 datum, 

517 self.row["Naif_id"], 

518 self.row["rotation"], 

519 CrsType.OGRAPHIC, 

520 ) 

521 

522 

523class Conversion: 

524 """Projection elements.""" 

525 

526 TEMPLATE_PARAMETER = """PARAMETER["$parameter_name", $parameter_value, 

527 $unit, 

528 ID["$authority", $authority_code]]""" 

529 

530 TEMPLATE_CONVERSION = """CONVERSION["$conversion_name", 

531 METHOD["$method_name", 

532 ID["$authority", $authority_code]], 

533 $params],""" 

534 

535 def __init__( 

536 self, 

537 conversion_name: str, 

538 method_name: str, 

539 method_id: str, 

540 projection: List[str], 

541 ) -> None: 

542 """Creates the projection elements. 

543 

544 Args: 

545 conversion_name (str): name of the projection 

546 method_name (str): name of the method 

547 method_id (str): method ID 

548 projection (List[str]): projection elements 

549 """ 

550 self.__conversion_name = conversion_name 

551 self.__method_name = method_name 

552 self.__method_id = method_id 

553 self.__projection = projection 

554 

555 @property 

556 def conversion_name(self) -> str: 

557 """Returns the conversion name. 

558 

559 Returns: 

560 str: the conversion 

561 """ 

562 return self.__conversion_name 

563 

564 @property 

565 def method_name(self) -> str: 

566 """Returns the method name. 

567 

568 Returns: 

569 str: the method name 

570 """ 

571 return self.__method_name 

572 

573 @property 

574 def method_id(self) -> str: 

575 """Returns the method ID. 

576 

577 Returns: 

578 str: the method ID 

579 """ 

580 return self.__method_id 

581 

582 @property 

583 def projection(self) -> List[str]: 

584 """Returns the projection elements. 

585 

586 Returns: 

587 List[str]: the projection elements 

588 """ 

589 return self.__projection 

590 

591 def wkt(self) -> str: 

592 """Returns the WKT of the projection elements. 

593 

594 Returns: 

595 str: the WKT 

596 """ 

597 parameter_template = Template(Conversion.TEMPLATE_PARAMETER) 

598 parameters = np.array( 

599 [ 

600 param 

601 for param in self.projection[3 : len(self.projection)] 

602 if param is not None 

603 ] 

604 ) 

605 parameters = parameters.reshape(int(len(parameters) / 2), 2) 

606 params: List[str] = list() 

607 for parameter in parameters: 

608 method_and_map: List[ 

609 str 

610 ] = ProjectionBody.METHOD_AND_PARAM_MAPPING[parameter[0]] 

611 param: str = parameter_template.substitute( 

612 parameter_name=parameter[0], 

613 parameter_value=parameter[1], 

614 unit=method_and_map[2], 

615 authority=method_and_map[0], 

616 authority_code=method_and_map[1], 

617 ) 

618 params.append(param) 

619 conversion_template = Template(Conversion.TEMPLATE_CONVERSION) 

620 

621 conversion = conversion_template.substitute( 

622 conversion_name=self.projection[1], 

623 method_name=self.projection[2], 

624 authority=ProjectionBody.METHOD_AND_PARAM_MAPPING[ 

625 self.projection[2] 

626 ][0], 

627 authority_code=ProjectionBody.METHOD_AND_PARAM_MAPPING[ 

628 self.projection[2] 

629 ][1], 

630 params=",\n\t\t".join(params), 

631 ) 

632 return conversion 

633 

634 

635@ICrs.register 

636class ProjectionBody(ICrs): 

637 """Coordinate Reference System of the projected body.""" 

638 

639 PROJECTION_DATA = [ 

640 [ 

641 10, 

642 "Equirectangular, clon = 0", 

643 "Equidistant Cylindrical", 

644 "Latitude of 1st standard parallel", 

645 0, 

646 "Longitude of natural origin", 

647 0, 

648 "False easting", 

649 0, 

650 "False northing", 

651 0, 

652 None, 

653 None, 

654 None, 

655 None, 

656 ], 

657 [ 

658 15, 

659 "Equirectangular, clon = 180", 

660 "Equidistant Cylindrical", 

661 "Latitude of 1st standard parallel", 

662 0, 

663 "Longitude of natural origin", 

664 180, 

665 "False easting", 

666 0, 

667 "False northing", 

668 0, 

669 None, 

670 None, 

671 None, 

672 None, 

673 ], 

674 [ 

675 20, 

676 "Sinusoidal, clon = 0", 

677 "Sinusoidal", 

678 "Longitude of natural origin", 

679 0, 

680 "False easting", 

681 0, 

682 "False northing", 

683 0, 

684 None, 

685 None, 

686 None, 

687 None, 

688 None, 

689 None, 

690 ], 

691 [ 

692 25, 

693 "Sinusoidal, clon = 180", 

694 "Sinusoidal", 

695 "Longitude of natural origin", 

696 180, 

697 "False easting", 

698 0, 

699 "False northing", 

700 0, 

701 None, 

702 None, 

703 None, 

704 None, 

705 None, 

706 None, 

707 ], 

708 [ 

709 30, 

710 "North Polar", 

711 "Polar Stereographic (variant A)", 

712 "Latitude of natural origin", 

713 90, 

714 "Longitude of natural origin", 

715 0, 

716 "Scale factor at natural origin", 

717 1, 

718 "False easting", 

719 0, 

720 "False northing", 

721 0, 

722 None, 

723 None, 

724 ], 

725 [ 

726 35, 

727 "South Polar", 

728 "Polar Stereographic (variant A)", 

729 "Latitude of natural origin", 

730 -90, 

731 "Longitude of natural origin", 

732 0, 

733 "Scale factor at natural origin", 

734 1, 

735 "False easting", 

736 0, 

737 "False northing", 

738 0, 

739 None, 

740 None, 

741 ], 

742 [ 

743 40, 

744 "Mollweide, clon = 0", 

745 "Mollweide", 

746 "Longitude of natural origin", 

747 0, 

748 "False easting", 

749 0, 

750 "False northing", 

751 0, 

752 None, 

753 None, 

754 None, 

755 None, 

756 None, 

757 None, 

758 ], 

759 [ 

760 45, 

761 "Mollweide, clon = 180", 

762 "Mollweide", 

763 "Longitude of natural origin", 

764 180, 

765 "False easting", 

766 0, 

767 "False northing", 

768 0, 

769 None, 

770 None, 

771 None, 

772 None, 

773 None, 

774 None, 

775 ], 

776 [ 

777 50, 

778 "Robinson, clon = 0", 

779 "Robinson", 

780 "Longitude of natural origin", 

781 0, 

782 "False easting", 

783 0, 

784 "False northing", 

785 0, 

786 None, 

787 None, 

788 None, 

789 None, 

790 None, 

791 None, 

792 ], 

793 [ 

794 55, 

795 "Robinson, clon = 180", 

796 "Robinson", 

797 "Longitude of natural origin", 

798 180, 

799 "False easting", 

800 0, 

801 "False northing", 

802 0, 

803 None, 

804 None, 

805 None, 

806 None, 

807 None, 

808 None, 

809 ], 

810 [ 

811 60, 

812 "Tranverse Mercator", 

813 "Transverse Mercator", 

814 "Latitude of natural origin", 

815 0, 

816 "Longitude of natural origin", 

817 0, 

818 "Scale factor at natural origin", 

819 1, 

820 "False easting", 

821 0, 

822 "False northing", 

823 0, 

824 None, 

825 None, 

826 ], 

827 [ 

828 65, 

829 "Orthographic, clon = 0", 

830 "Orthographic", 

831 "Latitude of natural origin", 

832 0, 

833 "Longitude of natural origin", 

834 0, 

835 "False easting", 

836 0, 

837 "False northing", 

838 0, 

839 None, 

840 None, 

841 None, 

842 None, 

843 ], 

844 [ 

845 70, 

846 "Orthographic, clon = 180", 

847 "Orthographic", 

848 "Latitude of natural origin", 

849 0, 

850 "Longitude of natural origin", 

851 180, 

852 "False easting", 

853 0, 

854 "False northing", 

855 0, 

856 None, 

857 None, 

858 None, 

859 None, 

860 ], 

861 [ 

862 75, 

863 "Lambert Conic Conformal", 

864 "Lambert Conic Conformal (2SP)", 

865 "Latitude of false origin", 

866 40, 

867 "Longitude of false origin", 

868 0, 

869 "Latitude of 1st standard parallel", 

870 20, 

871 "Latitude of 2nd standard parallel", 

872 60, 

873 "Easting at false origin", 

874 0, 

875 "Northing at false origin", 

876 0, 

877 ], 

878 [ 

879 80, 

880 "Lambert Azimuthal Equal Area", 

881 "Lambert Azimuthal Equal Area", 

882 "Latitude of natural origin", 

883 40, 

884 "Longitude of natural origin", 

885 0, 

886 "False easting", 

887 0, 

888 "False northing", 

889 0, 

890 None, 

891 None, 

892 None, 

893 None, 

894 ], 

895 [ 

896 85, 

897 "Albers Equal Area", 

898 "Albers Equal Area", 

899 "Latitude of false origin", 

900 40, 

901 "Longitude of false origin", 

902 0, 

903 "Latitude of 1st standard parallel", 

904 20, 

905 "Latitude of 2nd standard parallel", 

906 60, 

907 "Easting at false origin", 

908 0, 

909 "Northing at false origin", 

910 0, 

911 ], 

912 ] 

913 

914 METHOD_AND_PARAM_MAPPING: Dict[str, List] = { 

915 "Lambert Azimuthal Equal Area (Spherical)": ["EPSG", 1027], 

916 "Equidistant Cylindrical": ["EPSG", 1028], 

917 "Equidistant Cylindrical (Spherical)": ["EPSG", 1029], 

918 "Scale factor at natural origin": [ 

919 "EPSG", 

920 8805, 

921 'SCALEUNIT["unity",1,ID["EPSG", 9201]]', 

922 ], 

923 "False easting": [ 

924 "EPSG", 

925 8806, 

926 'LENGTHUNIT["metre",1,ID["EPSG", 9001]]', 

927 ], 

928 "False northing": [ 

929 "EPSG", 

930 8807, 

931 'LENGTHUNIT["metre",1,ID["EPSG", 9001]]', 

932 ], 

933 "Latitude of natural origin": [ 

934 "EPSG", 

935 8801, 

936 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG", 9122]]', 

937 ], 

938 "Longitude of natural origin": [ 

939 "EPSG", 

940 8802, 

941 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG", 9122]]', 

942 ], 

943 "Latitude of false origin": [ 

944 "EPSG", 

945 8821, 

946 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG", 9122]]', 

947 ], 

948 "Longitude of false origin": [ 

949 "EPSG", 

950 8822, 

951 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG", 9122]]', 

952 ], 

953 "Latitude of 1st standard parallel": [ 

954 "EPSG", 

955 8823, 

956 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG", 9122]]', 

957 ], 

958 "Latitude of 2nd standard parallel": [ 

959 "EPSG", 

960 8824, 

961 'ANGLEUNIT["degree",0.0174532925199433,ID["EPSG", 9122]]', 

962 ], 

963 "Easting at false origin": [ 

964 "EPSG", 

965 8826, 

966 'LENGTHUNIT["metre",1,ID["EPSG", 9001]]', 

967 ], 

968 "Northing at false origin": [ 

969 "EPSG", 

970 8827, 

971 'LENGTHUNIT["metre",1,ID["EPSG", 9001]]', 

972 ], 

973 "Sinusoidal": ["PROJ", '"SINUSOIDAL"'], 

974 "Robinson": ["PROJ", '"ROBINSON"'], 

975 "Mollweide": ["PROJ", '"MOLLWEIDE"'], 

976 "Transverse Mercator": ["EPSG", 9807], 

977 "Lambert Conic Conformal (2SP)": ["EPSG", 9802], 

978 "Polar Stereographic (variant A)": ["EPSG", 9810], 

979 "Lambert Azimuthal Equal Area": ["EPSG", 9820], 

980 "Albers Equal Area": ["EPSG", 9822], 

981 "Orthographic": ["EPSG", 9840], 

982 } 

983 

984 TEMPLATE_OCENTRIC_SPHERE = """PROJCRS["$projection_name", 

985 BASEGEOGCRS["$name ($version) $reference", 

986 $datum, 

987 ID["IAU", $number_body, $version]], 

988 $conversion 

989 CS[Cartesian, 2], 

990 AXIS["$direction_name", $direction, 

991 ORDER[1], 

992 LENGTHUNIT["metre", 1]], 

993 AXIS["Northing (N)", north, 

994 ORDER[2], 

995 LENGTHUNIT["metre", 1]], 

996 ID["IAU", $number, $version]]""" 

997 

998 TEMPLATE_OCENTRIC = """PROJCRS["$projection_name", 

999 BASEGEODCRS["$name ($version) $reference", 

1000 $datum, 

1001 ID["IAU", $number_body, $version]], 

1002 $conversion 

1003 CS[Cartesian, 2], 

1004 AXIS["$direction_name", $direction, 

1005 ORDER[1], 

1006 LENGTHUNIT["metre", 1]], 

1007 AXIS["Northing (N)", north, 

1008 ORDER[2], 

1009 LENGTHUNIT["metre", 1]], 

1010 ID["IAU", $number, $version]]""" 

1011 

1012 TEMPLATE_OGRAPHIC = """PROJCRS["$projection_name", 

1013 BASEGEOGCRS["$name ($version) $reference", 

1014 $datum, 

1015 ID["IAU", $number_body, $version]], 

1016 $conversion 

1017 CS[Cartesian, 2], 

1018 AXIS["$direction_name", $direction, 

1019 ORDER[1], 

1020 LENGTHUNIT["metre", 1]], 

1021 AXIS["Northing (N)", north, 

1022 ORDER[2], 

1023 LENGTHUNIT["metre", 1]], 

1024 ID["IAU", $number, $version]]""" 

1025 

1026 def __init__( 

1027 self, body_crs: BodyCrs, projection: List[str], template: str 

1028 ) -> None: 

1029 """Creates the projected body. 

1030 

1031 Args: 

1032 body_crs (BodyCrs): Coordinate Reference System of the body 

1033 projection (List[str]): projection elements 

1034 """ 

1035 self.__body_crs: BodyCrs = body_crs 

1036 self.__projection: List[str] = projection 

1037 self.__template: str = template 

1038 self.__conversion: Conversion = self._create_conversion( 

1039 projection[1], projection[2], "METHOD", projection 

1040 ) 

1041 

1042 def _create_conversion( # pylint: disable=no-self-use 

1043 self, 

1044 conversion_name: str, 

1045 method_name: str, 

1046 method_id: str, 

1047 projection: List[str], 

1048 ) -> Conversion: 

1049 """Create the conversion. 

1050 

1051 Args: 

1052 conversion_name (str): conversion name 

1053 method_name (str): method name 

1054 method_id (str): method ID 

1055 projection (List[str]): projection elements 

1056 

1057 Returns: 

1058 Conversion: Coversion 

1059 """ 

1060 return Conversion(conversion_name, method_name, method_id, projection) 

1061 

1062 def _create_projection(self) -> str: 

1063 """Returns the projection name. 

1064 

1065 Returns: 

1066 str: the projection name 

1067 """ 

1068 projection: str = ( 

1069 f"{self.body_crs.datum.body.name} ({IAU_REPORT.VERSION}) " 

1070 ) 

1071 if ( 

1072 self.body_crs.crs_type == CrsType.OCENTRIC 

1073 and self.body_crs.datum.body.shape == ReferenceShape.SPHERE 

1074 ): 

1075 reference = ( 

1076 projection 

1077 + "- " 

1078 + self.body_crs.datum.body.shape.value 

1079 + " / " 

1080 + self.body_crs.crs_type.value 

1081 + f"/ {self.projection[1]}" 

1082 ) 

1083 else: 

1084 reference = ( 

1085 projection 

1086 + "/ " 

1087 + self.body_crs.crs_type.value 

1088 + f"/ {self.projection[1]}" 

1089 ) 

1090 return reference 

1091 

1092 def _create_reference(self) -> str: 

1093 """Returns the reference name. 

1094 

1095 Returns: 

1096 str: the reference name 

1097 """ 

1098 reference: str 

1099 if ( 

1100 self.body_crs.crs_type == CrsType.OCENTRIC 

1101 and self.body_crs.datum.body.shape == ReferenceShape.SPHERE 

1102 ): 

1103 reference = ( 

1104 "- " 

1105 + self.body_crs.datum.body.shape.value 

1106 + " / " 

1107 + self.body_crs.crs_type.value 

1108 ) 

1109 else: 

1110 reference = "/ " + self.body_crs.crs_type.value 

1111 return reference 

1112 

1113 @property 

1114 def body_crs(self) -> BodyCrs: 

1115 """Returns the body description. 

1116 

1117 Returns: 

1118 str: the body description 

1119 """ 

1120 return self.__body_crs 

1121 

1122 @property 

1123 def projection(self) -> List[str]: 

1124 """Returns the projection elements. 

1125 

1126 Returns: 

1127 str: the projection elements 

1128 """ 

1129 return self.__projection 

1130 

1131 @property 

1132 def template(self) -> str: 

1133 """Returns the template. 

1134 

1135 Returns: 

1136 str: the template 

1137 """ 

1138 return self.__template 

1139 

1140 @property 

1141 def iau_code(self) -> int: 

1142 """Returns the IAU code. 

1143 

1144 Returns: 

1145 int: the IAU code 

1146 """ 

1147 return self.body_crs.iau_code + int(self.projection[0]) 

1148 

1149 @property 

1150 def datum(self) -> Datum: 

1151 """Datum. 

1152 

1153 :getter: Returns the datum 

1154 :type: Datum 

1155 """ 

1156 return self.body_crs.datum 

1157 

1158 @staticmethod 

1159 def create(body_crs: BodyCrs, projection: List[str]) -> "ProjectionBody": 

1160 """Create a projected coordinate reference system. 

1161 

1162 Args: 

1163 body_crs (BodyCrs): body CRS description 

1164 projection (List[str]): projection elements 

1165 

1166 Raises: 

1167 ValueError: Unknown CRS type 

1168 

1169 Returns: 

1170 ProjectionBody: projected CRS description 

1171 """ 

1172 result: ProjectionBody 

1173 if ( 

1174 body_crs.crs_type == CrsType.OCENTRIC 

1175 and body_crs.datum.body.shape == ReferenceShape.SPHERE 

1176 ): 

1177 result = ProjectionBody( 

1178 body_crs, projection, ProjectionBody.TEMPLATE_OCENTRIC_SPHERE 

1179 ) 

1180 elif body_crs.crs_type == CrsType.OCENTRIC: 

1181 result = ProjectionBody( 

1182 body_crs, projection, ProjectionBody.TEMPLATE_OCENTRIC 

1183 ) 

1184 elif body_crs.crs_type == CrsType.OGRAPHIC: 

1185 result = ProjectionBody( 

1186 body_crs, projection, ProjectionBody.TEMPLATE_OGRAPHIC 

1187 ) 

1188 else: 

1189 raise ValueError(f"Unknown CRS type: {CrsType}") 

1190 return result 

1191 

1192 def wkt(self) -> str: 

1193 """Returns the WKT of the projected body. 

1194 

1195 Returns: 

1196 str: the WKT of the projected body 

1197 """ 

1198 proj_body_template = Template(self.template) 

1199 return proj_body_template.substitute( 

1200 projection_name=self._create_projection(), 

1201 name=self.body_crs.name, 

1202 version=IAU_REPORT.VERSION, 

1203 datum=self.body_crs.datum.wkt(), 

1204 number=self.body_crs.iau_code + int(self.projection[0]), 

1205 number_body=self.body_crs.iau_code, 

1206 conversion=self.__conversion.wkt(), 

1207 reference=self._create_reference(), 

1208 direction=self.body_crs.direction, 

1209 direction_name="Westing (W)" 

1210 if self.body_crs.direction == "west" 

1211 else "Easting (E)", 

1212 ) 

1213 

1214 @staticmethod 

1215 def iter_projection(body_crs: BodyCrs) -> Generator: 

1216 """Iter on the different projections of the projected body 

1217 

1218 Args: 

1219 body_crs (BodyCrs): Body to project 

1220 

1221 Yields: 

1222 Generator: Iterator of the projections of the body. 

1223 """ 

1224 for projection in ProjectionBody.PROJECTION_DATA: 

1225 yield ProjectionBody.create(body_crs, cast(List[str], projection))