├── A4_sample_almanac.pdf ├── A4chartNorth_P.pdf ├── Explanation.pdf ├── LICENSE ├── README-Enno.md ├── README.md ├── Ra.jpg ├── alma_ephem.py ├── config.py ├── eventtables.py ├── increments.py ├── nautical.py ├── pyalmanac.py └── suntables.py /A4_sample_almanac.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodegerdts/Pyalmanac/27862dd34e8c09ba2879dd56e63fbfaddaa1bd6d/A4_sample_almanac.pdf -------------------------------------------------------------------------------- /A4chartNorth_P.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodegerdts/Pyalmanac/27862dd34e8c09ba2879dd56e63fbfaddaa1bd6d/A4chartNorth_P.pdf -------------------------------------------------------------------------------- /Explanation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodegerdts/Pyalmanac/27862dd34e8c09ba2879dd56e63fbfaddaa1bd6d/Explanation.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README-Enno.md: -------------------------------------------------------------------------------- 1 | # Pyalmanac (Python 3 version) 2 | 3 | **Description** 4 | 5 | Pyalmanac is a Python 3 script that creates the daily pages of the Nautical Almanac using the UTC timescale, 6 | which is the basis for the worldwide system of civil time. Official Nautical Almanacs employ a UT timescale (equivalent to UT1). 7 | 8 | The 'daily pages' are tables that are needed for celestial navigation with a sextant. 9 | Although you are strongly advised to purchase the official Nautical Almanac, this program will reproduce the tables with no warranty or guarantee of accuracy. 10 | 11 | Pyalmanac was developed based on the original *Pyalmanac* by Enno Rodegerdts. Various improvements, enhancements and bugfixes have been added since. 12 | 13 | **Current state of Pyalmanac** 14 | 15 | Pyalmanac is a somewhat dated program. 16 | Pyalmanac is implemented using the [Ephem](https://rhodesmill.org/pyephem/) astronomical library (originally named PyEphem), which has *Mature* development status. 17 | Ephem was based upon XEphem, which is 'end of life' as no further updates to XEphem are planned. 18 | Elwood Charles Downey, the author of XEphem, generously gave permission for their use in (Py)Ephem. 19 | **Please note the Software Requirements below for Ephem as the latest versions still contain a software error!** 20 | 21 | Pyalmanac contains its own star database, now updated with data from the Hipparcos Star Catalogue. 22 | Star names are chosen to comply with official Nautical Almanacs. 23 | The GHA/Dec star data now matches a sample page from a Nautical Almanac typically to within 0°0.1'. 24 | As of now, (Py)Ephem will continue to receive critical bugfixes and be ported to each new version of Python. 25 | Pyalmanac still has the advantage of speed over other implementations. 26 | 27 | One minor limitation of Ephem is in the EOP (Earth Orientation Parameters) (affecting *sidereal time*) which is more accurate in Skyfield-based almanacs as they can employ the IERS (International Earth Rotation Service) EOP published data. This affects *sidereal time*, which minimizes GHA discrepancies in general. (This applies to all celestial objects.) 28 | 29 | Slight differences can also be detected in the *Event Times*: sunrise, sunset, moonrise, moonset, civil twilight start/end and nautical twilight start/end, particularly in more Northern latitudes. 30 | 31 | Given the choice, [SFalmanac](https://pypi.org/project/sfalmanac/) is an up-to-date program with almost identical functionality to Pyalmanac, and it uses [Skyfield](https://rhodesmill.org/skyfield/), a modern astronomical library based on the highly accurate algorithms employed in the [NASA JPL HORIZONS System](https://ssd.jpl.nasa.gov/horizons/). 32 | (Pyalmanac and SFalmanac have same formatted pages so differences can easily be spotted by swiching between them in Adobe Acrobat reader.) 33 | 34 | **ACKNOWLEDGEMENTS** 35 | 36 | I, Andrew Bauer, wish to thank Enno Rodegerdts for permission to update his GitHub site. Without Enno's pioneering work on the original Python 2 version of Pyalmanac I would never have started (or known how to start) on this journey. I also thank the experts who have helped me... especially Brandon Rhodes, author of Ephem/PyEphem and Skyfield, for his assistance. 37 | 38 | **AVAILABLE VERSIONS** 39 | 40 | **NOTE: the Python Package Index (PyPI) edition is here:** https://pypi.org/project/pyalmanac/ 41 | **Users are encouraged to install the PyPI edition instead.** 42 | 43 | Two versions (other than Pyalmanac) are available here: https://github.com/aendie 44 | 45 | * **Pyalmanac** is the fastest with marginally poorer accuracy. 46 | * **SFalmanac** is slightly slower and most accurate; based on *Skyfield*. *Ephem* is only used for a few planet magnitudes (because these are not yet in *Skyfield*). 47 | * **Skyalmanac** is a hybrid version that is no longer recommended now that SFalmanac supports multiprocessing. 48 | 49 | Ephem website: https://rhodesmill.org/pyephem/ 50 | Skyfield website: https://rhodesmill.org/skyfield/ 51 | 52 | **UPDATE: Mar 2020** 53 | 54 | A new parameter in *config.py* enables one to choose between A4 and Letter-sized pages. A [new approach](https://docs.python.org/3/whatsnew/3.0.html#pep-3101-a-new-approach-to-string-formatting) to string formatting has been implemented: 55 | the [old](https://docs.python.org/2/library/stdtypes.html#string-formatting) style Python string formatting syntax has been replaced by the [new](https://docs.python.org/3/library/string.html#format-string-syntax) style string formatting syntax. 56 | 57 | **UPDATE: Jun 2020** 58 | 59 | The Equation Of Time is shaded whenever EoT is negative indicating that apparent solar time is slow compared to mean solar time (mean solar time > apparent solar time). 60 | 61 | **UPDATE: Feb 2021** 62 | 63 | Minor changes are included here to this original (non-PyPI) edition to reflect some of the adaptation that was required (e.g. integrate *increments.py* into *pyalmanac.py* as Option 5) to create a PyPI (Python Package Index) edition making this original (non-PyPI) and the PyPI editions similar. Both editions create identical almanacs and the [PyPI edition](https://pypi.org/project/pyalmanac/) is the preferred choice for users. 64 | 65 | **UPDATE: Apr 2021** 66 | 67 | A double moonrise or moonset on the same day is now highlighted for better legibility. Event Time tables can now be generated - these are the tables containing data in hours:minutes:seconds, e.g. sunrise, sunset, moonrise, moonset and Meridian Passage. 68 | Accuracy to to the second of time is not required for navigational purposes, but may be used to compare accuracy with other algorithms. Some internal technical enhancements and minor changes to text are also included. 69 | 70 | **UPDATE: May 2021** 71 | 72 | The indication of objects (Sun or Moon) continuously above or below the horizon has been corrected. 73 | 74 | Regarding Moon Data: ".. .." has been added to indicate that the moonrise/moonset event occurs the following day (at the specified latitude). If there is no moonrise/moonset for two or more consecutive days, black boxes indicate "moon below horizon"; white boxes indicate "moon above horizon". This brings it in line with Nautical Almanacs. (Previously they were only displayed when there was no moonrise *and* no moonset on a single day.) 75 | 76 | Correction to Sun Data: "Sun continually above/below horizon" now shown if it applies to both Sunrise and Sunset, or *additionally* to both Civil Twilight Start & End; or *additionally* to both Astronomical Twilight Start & End, i.e. as two, four or six events per day and latitude. This brings it in line with Nautical Almanacs. 77 | 78 |  :smiley: Pyalmanac is now available on DockerHub [here](https://hub.docker.com/repository/docker/aendie/pyalmanac). :smiley: 79 | 80 | The DockerHub image contains a Linux-based OS, TeX Live, the application code, and third party Python imports (including the astronomical library). It can be executed "in a container" on Windows 10 Pro, macOS or a Linux-based OS. 81 | 82 | **UPDATE: Jul 2021** 83 | 84 | The PDF filenames have been revised: 85 | 86 | * modna_\.pdf: for Nautical Almanacs in modern style 87 | * modst_\.pdf: for Sun Tables in modern style 88 | * tradna_\.pdf: for Nautical Almanacs in traditional style 89 | * tradst_\.pdf: for Sun Tables in traditional style 90 | 91 | One command line argument may be appended to the run command: 92 | 93 | * -v to invoke verbose mode (send pdfTeX execution steps to the console) 94 | * -log to preserve the log file 95 | * -tex to preserve the tex file 96 | 97 | **UPDATE: Nov 2021** 98 | 99 | * Enhanced User Interface includes the possibility to generate tables starting at any valid date, or for any month (within -12/+11 months from today). 100 | * Minor cosmetic improvements ('d'-correction in italics; greek 'nu' replaces 'v'-correction; Minutes-symbol added to SD and d) 101 | 102 | Increased accuracy due to the following minor improvements: 103 | * Moon phase (percent illumination) is based on midnight (as opposed to midday) 104 | * Star positions are based on midnight (as opposed to midday) 105 | * Moon v and d for hour ‘n’ are based on “hour ‘n+1’ minus hour ‘n’” as opposed to “hour ‘n’ + 30 minutes minus hour ‘n’ – 30 minutes” 106 | 107 | The PDF filenames have been revised (again): 108 | 109 | * NAmod_\.pdf: for Nautical Almanacs in modern style 110 | * STmod_\.pdf: for Sun Tables in modern style 111 | * NAtrad_\.pdf: for Nautical Almanacs in traditional style 112 | * STtrad_\.pdf: for Sun Tables in traditional style 113 | 114 | **UPDATE: Sep 2022** 115 | 116 | * The PDF filenames have been harmonized with those in SFalmanac. 117 | * Better support for Letter-sized pages. 118 | * The LaTeX *fancyhdr* package is employed when MiKTeX (or a TeX Live version >= 2020) is detected. 119 | * Command line options: 120 | * -v ... 'verbose': to send pdfTeX output to the terminal 121 | * -log ... to keep the log file 122 | * -tex ... to keep the tex file 123 | * -old ... old formatting without the fancyhdr package 124 | * -a4 ... A4 papersize 125 | * -let ... Letter papersize 126 | * -dpo ... data pages only 127 | 128 | ## Requirements 129 | 130 |  Astronomical computation is done by the free Ephem library. 131 |  Typesetting is typically done by MiKTeX or TeX Live. 132 |  Here are the requirements/recommendations: 133 | 134 | * Python v3.4 or higher (the latest version is recommended) 135 | * Ephem >= 3.7.6 (4.1 is good; 4.1.1, 4.1.2 or 4.1.3 are faulty) 136 | * MiKTeX or TeX Live 137 | 138 | ## Files required in the execution folder: 139 | 140 | * *.py 141 | * Ra.jpg 142 | * A4chartNorth_P.pdf 143 | 144 | ### INSTALLATION GUIDELINES on Windows 10: 145 | 146 |  Install Python 3.10.6 It should be in the system environment variable PATH, e.g. 147 |   **C:\\Python310\Scripts;C:\\Python310;** ..... 148 |  Install MiKTeX 22.7 from https://miktex.org/ 149 |  When MiKTeX first runs it will require installation of additional packages. 150 |  Run Command Prompt as Administrator, go to your Python folder and execute, e.g.: 151 | 152 |  **cd C:\\Python310** 153 |  **python.exe -m pip install --upgrade pip** 154 |  ... for a first install: 155 |  **pip3 uninstall pyephem ephem** 156 |  **pip3 install ephem==4.1** 157 | 158 |  Put the Pyalmanac files in a new folder, run Command Prompt and start with: 159 |  **py -3 pyalmanac.py** 160 | 161 |  If using MiKTeX 21 or higher, executing 'option 7' (Increments and Corrections) will probably fail with 162 |  **! TeX capacity exceeded, sorry [main memory size=3000000].** 163 |  To resolve this problem (assuming MiKTeX has been installed for all users), 164 |  open a Command Prompt as Administrator and enter: 165 |  **initexmf --admin --edit-config-file=pdflatex** 166 |  This opens **pdflatex.ini** in Notepad. Add the following line: 167 |  **extra_mem_top = 1000000** 168 |  and save the file. Problem solved. For more details go [here](https://tex.stackexchange.com/questions/438902/how-to-increase-memory-size-for-xelatex-in-miktex/438911#438911) 169 | 170 | ### INSTALLATION GUIDELINES on Ubuntu 19.10 or 20.04: 171 | 172 |  Ubuntu 19 and higher come with Python 3 preinstalled, 173 |  however pip may need to be installed: 174 |  **sudo apt install python3-pip** 175 | 176 |  Install the following TeX Live package: 177 |  **sudo apt install texlive-latex-extra** 178 | 179 |  Install the required astronomical library: 180 |  **pip3 uninstall pyephem ephem** 181 |  **pip3 install ephem==4.1** 182 | 183 |  Put the Pyalmanac files in a folder and start with: 184 |  **python3 pyalmanac.py** 185 | 186 | 187 | ### INSTALLATION GUIDELINES on MAC: 188 | 189 |  Every Mac comes with python preinstalled. 190 |  (Please choose this version of Pyalmanac if Python 3.* is installed.) 191 |  You need to install the Ephem library to use Pyalmanac. 192 |  Type the following commands at the commandline (terminal app): 193 | 194 |  **sudo easy_install pip** 195 |  **pip uninstall pyephem ephem** 196 |  **pip install ephem==4.1** 197 | 198 |  If this command fails, your Mac asks you if you would like to install the header files. 199 |  Do so - you do not need to install the full IDE - and try again. 200 | 201 |  Install TeX/LaTeX from http://www.tug.org/mactex/ 202 | 203 |  Now you are almost ready. Put the Pyalmanac files in any directory and start with: 204 |  **python pyalmanac** 205 |  or 206 |  **./pyalmanac** -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyalmanac (Python 3 version) 2 | 3 | **Description** 4 | 5 | Pyalmanac is a Python 3 script that creates the daily pages of the Nautical Almanac using the UTC timescale, 6 | which is the basis for the worldwide system of civil time. Official Nautical Almanacs employ a UT timescale (equivalent to UT1). 7 | 8 | The 'daily pages' are tables that are needed for celestial navigation with a sextant. 9 | Although you are strongly advised to purchase the official Nautical Almanac, this program will reproduce the tables with no warranty or guarantee of accuracy. 10 | 11 | Pyalmanac was developed based on the original *Pyalmanac* by Enno Rodegerdts. Various improvements, enhancements and bugfixes have been added since. 12 | 13 | **Current state of Pyalmanac** 14 | 15 | Pyalmanac is a somewhat dated program. 16 | Pyalmanac is implemented using the [Ephem](https://rhodesmill.org/pyephem/) astronomical library (originally named PyEphem), which has *Mature* development status. 17 | Ephem was based upon XEphem, which is 'end of life' as no further updates to XEphem are planned. 18 | Elwood Charles Downey, the author of XEphem, generously gave permission for their use in (Py)Ephem. 19 | **Please note the Software Requirements below for Ephem as the latest versions still contain a software error!** 20 | 21 | Pyalmanac contains its own star database, now updated with data from the Hipparcos Star Catalogue. 22 | Star names are chosen to comply with official Nautical Almanacs. 23 | The GHA/Dec star data now matches a sample page from a Nautical Almanac typically to within 0°0.1'. 24 | As of now, (Py)Ephem will continue to receive critical bugfixes and be ported to each new version of Python. 25 | Pyalmanac still has the advantage of speed over other implementations. 26 | 27 | One minor limitation of Ephem is in the EOP (Earth Orientation Parameters) (affecting *sidereal time*) which is more accurate in Skyfield-based almanacs as they can employ the IERS (International Earth Rotation Service) EOP published data. This affects *sidereal time*, which minimizes GHA discrepancies in general. (This applies to all celestial objects.) 28 | 29 | Slight differences can also be detected in the *Event Times*: sunrise, sunset, moonrise, moonset, civil twilight start/end and nautical twilight start/end, particularly in more Northern latitudes. 30 | 31 | Given the choice, [SFalmanac](https://pypi.org/project/sfalmanac/) is an up-to-date program with almost identical functionality to Pyalmanac, and it uses [Skyfield](https://rhodesmill.org/skyfield/), a modern astronomical library based on the highly accurate algorithms employed in the [NASA JPL HORIZONS System](https://ssd.jpl.nasa.gov/horizons/). 32 | (Pyalmanac and SFalmanac have same formatted pages so differences can easily be spotted by swiching between them in Adobe Acrobat reader.) 33 | 34 | **ACKNOWLEDGEMENTS** 35 | 36 | I, Andrew Bauer, wish to thank Enno Rodegerdts for permission to update his GitHub site. Without Enno's pioneering work on the original Python 2 version of Pyalmanac I would never have started (or known how to start) on this journey. I also thank the experts who have helped me... especially Brandon Rhodes, author of Ephem/PyEphem and Skyfield, for his assistance. 37 | 38 | **AVAILABLE VERSIONS** 39 | 40 | **NOTE: the Python Package Index (PyPI) edition is here:** https://pypi.org/project/pyalmanac/ 41 | **Users are encouraged to install the PyPI edition instead.** 42 | 43 | Two versions (other than Pyalmanac) are available here: https://github.com/aendie 44 | 45 | * **Pyalmanac** is the fastest with marginally poorer accuracy. 46 | * **SFalmanac** is slightly slower and most accurate; based on *Skyfield*. *Ephem* is only used for a few planet magnitudes (because these are not yet in *Skyfield*). 47 | * **Skyalmanac** is a hybrid version that is no longer recommended now that SFalmanac supports multiprocessing. 48 | 49 | Ephem website: https://rhodesmill.org/pyephem/ 50 | Skyfield website: https://rhodesmill.org/skyfield/ 51 | 52 | **UPDATE: Mar 2020** 53 | 54 | A new parameter in *config.py* enables one to choose between A4 and Letter-sized pages. A [new approach](https://docs.python.org/3/whatsnew/3.0.html#pep-3101-a-new-approach-to-string-formatting) to string formatting has been implemented: 55 | the [old](https://docs.python.org/2/library/stdtypes.html#string-formatting) style Python string formatting syntax has been replaced by the [new](https://docs.python.org/3/library/string.html#format-string-syntax) style string formatting syntax. 56 | 57 | **UPDATE: Jun 2020** 58 | 59 | The Equation Of Time is shaded whenever EoT is negative indicating that apparent solar time is slow compared to mean solar time (mean solar time > apparent solar time). 60 | 61 | **UPDATE: Feb 2021** 62 | 63 | Minor changes are included here to this original (non-PyPI) edition to reflect some of the adaptation that was required (e.g. integrate *increments.py* into *pyalmanac.py* as Option 5) to create a PyPI (Python Package Index) edition making this original (non-PyPI) and the PyPI editions similar. Both editions create identical almanacs and the [PyPI edition](https://pypi.org/project/pyalmanac/) is the preferred choice for users. 64 | 65 | **UPDATE: Apr 2021** 66 | 67 | A double moonrise or moonset on the same day is now highlighted for better legibility. Event Time tables can now be generated - these are the tables containing data in hours:minutes:seconds, e.g. sunrise, sunset, moonrise, moonset and Meridian Passage. 68 | Accuracy to to the second of time is not required for navigational purposes, but may be used to compare accuracy with other algorithms. Some internal technical enhancements and minor changes to text are also included. 69 | 70 | **UPDATE: May 2021** 71 | 72 | The indication of objects (Sun or Moon) continuously above or below the horizon has been corrected. 73 | 74 | Regarding Moon Data: ".. .." has been added to indicate that the moonrise/moonset event occurs the following day (at the specified latitude). If there is no moonrise/moonset for two or more consecutive days, black boxes indicate "moon below horizon"; white boxes indicate "moon above horizon". This brings it in line with Nautical Almanacs. (Previously they were only displayed when there was no moonrise *and* no moonset on a single day.) 75 | 76 | Correction to Sun Data: "Sun continually above/below horizon" now shown if it applies to both Sunrise and Sunset, or *additionally* to both Civil Twilight Start & End; or *additionally* to both Astronomical Twilight Start & End, i.e. as two, four or six events per day and latitude. This brings it in line with Nautical Almanacs. 77 | 78 |  :smiley: Pyalmanac is now available on DockerHub [here](https://hub.docker.com/repository/docker/aendie/pyalmanac). :smiley: 79 | 80 | The DockerHub image contains a Linux-based OS, TeX Live, the application code, and third party Python imports (including the astronomical library). It can be executed "in a container" on Windows 10 Pro, macOS or a Linux-based OS. 81 | 82 | **UPDATE: Jul 2021** 83 | 84 | The PDF filenames have been revised: 85 | 86 | * modna_\.pdf: for Nautical Almanacs in modern style 87 | * modst_\.pdf: for Sun Tables in modern style 88 | * tradna_\.pdf: for Nautical Almanacs in traditional style 89 | * tradst_\.pdf: for Sun Tables in traditional style 90 | 91 | One command line argument may be appended to the run command: 92 | 93 | * -v to invoke verbose mode (send pdfTeX execution steps to the console) 94 | * -log to preserve the log file 95 | * -tex to preserve the tex file 96 | 97 | **UPDATE: Nov 2021** 98 | 99 | * Enhanced User Interface includes the possibility to generate tables starting at any valid date, or for any month (within -12/+11 months from today). 100 | * Minor cosmetic improvements ('d'-correction in italics; greek 'nu' replaces 'v'-correction; Minutes-symbol added to SD and d) 101 | 102 | Increased accuracy due to the following minor improvements: 103 | * Moon phase (percent illumination) is based on midnight (as opposed to midday) 104 | * Star positions are based on midnight (as opposed to midday) 105 | * Moon v and d for hour ‘n’ are based on “hour ‘n+1’ minus hour ‘n’” as opposed to “hour ‘n’ + 30 minutes minus hour ‘n’ – 30 minutes” 106 | 107 | The PDF filenames have been revised (again): 108 | 109 | * NAmod_\.pdf: for Nautical Almanacs in modern style 110 | * STmod_\.pdf: for Sun Tables in modern style 111 | * NAtrad_\.pdf: for Nautical Almanacs in traditional style 112 | * STtrad_\.pdf: for Sun Tables in traditional style 113 | 114 | **UPDATE: Sep 2022** 115 | 116 | * The PDF filenames have been harmonized with those in SFalmanac. 117 | * Better support for Letter-sized pages. 118 | * The LaTeX *fancyhdr* package is employed when MiKTeX (or a TeX Live version >= 2020) is detected. 119 | * Command line options: 120 | * -v ... 'verbose': to send pdfTeX output to the terminal 121 | * -log ... to keep the log file 122 | * -tex ... to keep the tex file 123 | * -old ... old formatting without the fancyhdr package 124 | * -a4 ... A4 papersize 125 | * -let ... Letter papersize 126 | * -dpo ... data pages only 127 | 128 | ## Requirements 129 | 130 |  Astronomical computation is done by the free Ephem library. 131 |  Typesetting is typically done by MiKTeX or TeX Live. 132 |  Here are the requirements/recommendations: 133 | 134 | * Python v3.4 or higher (the latest version is recommended) 135 | * Ephem >= 3.7.6 (4.1 is good; 4.1.1, 4.1.2 or 4.1.3 are faulty) 136 | * MiKTeX or TeX Live 137 | 138 | ## Files required in the execution folder: 139 | 140 | * *.py 141 | * Ra.jpg 142 | * A4chartNorth_P.pdf 143 | 144 | ### INSTALLATION GUIDELINES on Windows 10: 145 | 146 |  Install Python 3.10.6 It should be in the system environment variable PATH, e.g. 147 |   **C:\\Python310\Scripts;C:\\Python310;** ..... 148 |  Install MiKTeX 22.7 from https://miktex.org/ 149 |  When MiKTeX first runs it will require installation of additional packages. 150 |  Run Command Prompt as Administrator, go to your Python folder and execute, e.g.: 151 | 152 |  **cd C:\\Python310** 153 |  **python.exe -m pip install --upgrade pip** 154 |  ... for a first install: 155 |  **pip3 uninstall pyephem ephem** 156 |  **pip3 install ephem==4.1** 157 | 158 |  Put the Pyalmanac files in a new folder, run Command Prompt and start with: 159 |  **py -3 pyalmanac.py** 160 | 161 |  If using MiKTeX 21 or higher, executing 'option 7' (Increments and Corrections) will probably fail with 162 |  **! TeX capacity exceeded, sorry [main memory size=3000000].** 163 |  To resolve this problem (assuming MiKTeX has been installed for all users), 164 |  open a Command Prompt as Administrator and enter: 165 |  **initexmf --admin --edit-config-file=pdflatex** 166 |  This opens **pdflatex.ini** in Notepad. Add the following line: 167 |  **extra_mem_top = 1000000** 168 |  and save the file. Problem solved. For more details go [here](https://tex.stackexchange.com/questions/438902/how-to-increase-memory-size-for-xelatex-in-miktex/438911#438911) 169 | 170 | ### INSTALLATION GUIDELINES on Ubuntu 19.10 or 20.04: 171 | 172 |  Ubuntu 19 and higher come with Python 3 preinstalled, 173 |  however pip may need to be installed: 174 |  **sudo apt install python3-pip** 175 | 176 |  Install the following TeX Live package: 177 |  **sudo apt install texlive-latex-extra** 178 | 179 |  Install the required astronomical library: 180 |  **pip3 uninstall pyephem ephem** 181 |  **pip3 install ephem==4.1** 182 | 183 |  Put the Pyalmanac files in a folder and start with: 184 |  **python3 pyalmanac.py** 185 | 186 | 187 | ### INSTALLATION GUIDELINES on MAC: 188 | 189 |  Every Mac comes with python preinstalled. 190 |  (Please choose this version of Pyalmanac if Python 3.* is installed.) 191 |  You need to install the Ephem library to use Pyalmanac. 192 |  Type the following commands at the commandline (terminal app): 193 | 194 |  **sudo easy_install pip** 195 |  **pip uninstall pyephem ephem** 196 |  **pip install ephem==4.1** 197 | 198 |  If this command fails, your Mac asks you if you would like to install the header files. 199 |  Do so - you do not need to install the full IDE - and try again. 200 | 201 |  Install TeX/LaTeX from http://www.tug.org/mactex/ 202 | 203 |  Now you are almost ready. Put the Pyalmanac files in any directory and start with: 204 |  **python pyalmanac** 205 |  or 206 |  **./pyalmanac** -------------------------------------------------------------------------------- /Ra.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rodegerdts/Pyalmanac/27862dd34e8c09ba2879dd56e63fbfaddaa1bd6d/Ra.jpg -------------------------------------------------------------------------------- /alma_ephem.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # contains all functions that calculate values for the nautical almanac 5 | 6 | # Copyright (C) 2014 Enno Rodegerdts 7 | # Copyright (C) 2022 Andrew Bauer 8 | 9 | # This program is free software; you can redistribute it and/or modify 10 | # it under the terms of the GNU General Public License as published by 11 | # the Free Software Foundation; either version 2 of the License, or 12 | # (at your option) any later version. 13 | # 14 | # This program is distributed in the hope that it will be useful, 15 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | # GNU General Public License for more details. 18 | # 19 | # You should have received a copy of the GNU General Public License along 20 | # with this program; if not, write to the Free Software Foundation, Inc., 21 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 22 | 23 | ###### Standard library imports ###### 24 | # don't confuse the 'date' method with the 'Date' variable! 25 | from datetime import date 26 | from math import degrees, pi, tan 27 | import sys 28 | 29 | ###### Third party imports ###### 30 | import ephem 31 | 32 | ###### Local application imports ###### 33 | import config 34 | 35 | ephem_sun = ephem.Sun() 36 | ephem_moon = ephem.Moon() 37 | ephem_venus = ephem.Venus() 38 | ephem_mars = ephem.Mars() 39 | ephem_jupiter = ephem.Jupiter() 40 | ephem_saturn = ephem.Saturn() 41 | #degree_sign= u'\N{DEGREE SIGN}' 42 | 43 | #---------------------- 44 | # internal methods 45 | #---------------------- 46 | 47 | def hhmm(Date): 48 | # turn an ephem.date (float) into a time string formatted hh:mm 49 | tup = Date.tuple() 50 | hr = tup[-3] 51 | # round >=30 seconds to next minute 52 | min = tup[-2] + int(round((tup[-1]/60)+0.00001)) 53 | # nextday = False 54 | if min == 60: 55 | min = 0 56 | hr += 1 57 | if hr == 24: 58 | hr = 0 59 | # nextday = True # time rounded up into next day 60 | time = '{:02d}:{:02d}'.format(hr,min) # time = "%02d:%02d" %(hr,min) 61 | # return time, nextday 62 | # NOTE: this function could easily return the information that rounding 63 | # flipped into the next day, however this is not required here. 64 | return time 65 | 66 | def hhmmss(Date): # used by eventtables.py 67 | # turn an ephem.date (float) into a time string formatted hh:mm:ss 68 | tup = Date.tuple() 69 | hr = tup[-3] 70 | mi = tup[-2] 71 | sec = int(round(tup[-1])) 72 | # nextday = False 73 | if sec == 60: 74 | sec =0 75 | mi += 1 76 | if mi == 60: 77 | mi = 0 78 | hr += 1 79 | if hr == 24: 80 | hr = 0 81 | # nextday = True # time rounded up into next day 82 | time = '{:02d}:{:02d}:{:02d}'.format(hr,mi,sec) # time = "%02d:%02d:%02d" %(hr,min,sec) 83 | # return time, nextday 84 | # NOTE: this function could easily return the information that rounding 85 | # flipped into the next day, however this is not required here. 86 | return time 87 | 88 | def date2time(Date, withseconds = False): 89 | if withseconds: 90 | return hhmmss(Date) 91 | else: 92 | return hhmm(Date) 93 | 94 | def nadeg(rad, fixedwidth=1): 95 | # changes ephem.angle (rad) to the format usually used in the nautical almanac (ddd°mm.m) and returns a string object. 96 | # the optional argument specifies the minimum width for degrees (only) 97 | theminus = "" 98 | if rad < 0: 99 | theminus = '-' 100 | df = abs(degrees(rad)) # convert radians to degrees (float) 101 | di = int(df) # degrees (integer) 102 | # note: round() uses "Rounding Half To Even" strategy 103 | mf = round((df-di)*60, 1) # minutes (float), rounded to 1 decimal place 104 | mi = int(mf) # minutes (integer) 105 | if mi == 60: 106 | mf -= 60 107 | di += 1 108 | if di == 360: 109 | di = 0 110 | if fixedwidth == 2: 111 | gm = "{}{:02d}$^\circ${:04.1f}".format(theminus,di,mf) 112 | else: 113 | if fixedwidth == 3: 114 | gm = "{}{:03d}$^\circ${:04.1f}".format(theminus,di,mf) 115 | else: 116 | gm = "{}{}$^\circ${:04.1f}".format(theminus,di,mf) 117 | return gm 118 | 119 | def flag_msg(msg): 120 | if config.logfileopen: 121 | # if open - write to log file 122 | config.writeLOG(msg + '\n') 123 | else: 124 | # otherwise - print to console 125 | print(msg) 126 | return 127 | 128 | #------------------------------- 129 | # Sun and Moon calculations 130 | #------------------------------- 131 | 132 | def sunmoon(Date): # used in suntab(m), sunmoontab(m) 133 | # returns ephemrerids for sun and moon. 134 | 135 | #Sun gha dec 136 | #Moon gha v dec d hp 137 | 138 | obs = ephem.Observer() 139 | obs.date = Date 140 | 141 | #Sun 142 | ephem_sun.compute(Date,epoch=Date) 143 | deg = ephem.degrees(obs.sidereal_time()-ephem_sun.g_ra).norm 144 | ghas = nadeg(deg) 145 | degs = ephem_sun.g_dec 146 | decs = nadeg(degs,2) 147 | 148 | #Moon 149 | ephem_moon.compute(Date,epoch=Date) 150 | deg = ephem.degrees(obs.sidereal_time()-ephem_moon.g_ra).norm 151 | gham = nadeg(deg) 152 | degm = ephem_moon.g_dec 153 | decm = nadeg(degm,2) 154 | 155 | #calculate the moon's horizontal paralax 156 | deg = ephem.degrees(ephem_moon.radius/0.272805950305) 157 | hp = "{:0.1f}'".format(deg*360*30/pi) 158 | 159 | #calculate v and d by advancing the time with one hour. 160 | ephem_moon.compute(Date,epoch=Date) 161 | obs.date = Date 162 | rgha = ephem.degrees(obs.sidereal_time()-ephem_moon.g_ra).norm 163 | rdec = ephem_moon.g_dec 164 | ephem_moon.compute(Date+ephem.hour,epoch=Date+ephem.hour) 165 | obs.date = Date + ephem.hour 166 | rghap = ephem.degrees(obs.sidereal_time()-ephem_moon.g_ra).norm 167 | 168 | deg = ephem.degrees(ephem.degrees(rghap-rgha).norm-ephem.degrees('14:19:00')) 169 | vmf = deg*360*30/pi 170 | vm = "{:0.1f}'".format(vmf) 171 | 172 | deg = ephem.degrees(ephem_moon.g_dec-rdec) 173 | dmf = deg*360*30/pi 174 | dm = "{:0.1f}'".format(dmf) 175 | 176 | # degs, degm have been added for the sunmooontab function 177 | return ghas,decs,gham,vm,decm,dm,hp,degs,degm 178 | 179 | def sun_moon_SD(Date): # used in suntab(m), sunmoontab(m) 180 | obs = ephem.Observer() 181 | obs.date = Date 182 | 183 | #Sun 184 | # compute semi-diameter of sun and sun's declination change per hour (in minutes) 185 | ephem_sun.compute(Date) 186 | dec = ephem_sun.g_dec 187 | ephem_sun.compute(Date+ephem.hour) 188 | obs.date = Date+ephem.hour 189 | deg = ephem.degrees(ephem_sun.g_dec-dec) 190 | ds = "{:0.1f}".format(deg*360*30/pi) 191 | sds = "{:0.1f}".format(ephem_sun.radius*360*30/pi) 192 | 193 | #Moon 194 | # compute semi-diameter of moon (in minutes) 195 | ephem_moon.compute(Date) 196 | sdm = "{:0.1f}".format(ephem_moon.radius*360*30/pi) 197 | 198 | return ds,sds,sdm 199 | 200 | #------------------------------------------------ 201 | # Venus, Mars, Jupiter & Saturn calculations 202 | #------------------------------------------------ 203 | 204 | def planetsGHA(Date): # used in planetstab(m) 205 | # this function returns a tuple of strings with ephemerids in the format used by the nautical almanac. 206 | 207 | # following are objects and their values: 208 | #Aries gha 209 | #Venus gha dec 210 | #Mars gha dec 211 | #Jupiter gha dec 212 | #Saturn gha dec 213 | 214 | obs = ephem.Observer() 215 | obs.date = Date 216 | 217 | #Aries, First Point of 218 | deg = ephem.degrees(obs.sidereal_time()).norm 219 | ghaa = nadeg(deg) 220 | 221 | #Venus 222 | ephem_venus.compute(Date,epoch=Date) 223 | deg = ephem.degrees(obs.sidereal_time()-ephem_venus.g_ra).norm 224 | ghav = nadeg(deg) 225 | degv = ephem_venus.g_dec 226 | decv = nadeg(degv,2) 227 | 228 | #Mars 229 | ephem_mars.compute(Date,epoch=Date) 230 | deg = ephem.degrees(obs.sidereal_time()-ephem_mars.g_ra).norm 231 | ghamars = nadeg(deg) 232 | degmars = ephem_mars.g_dec 233 | decmars = nadeg(degmars,2) 234 | 235 | #Jupiter 236 | ephem_jupiter.compute(Date,epoch=Date) 237 | deg = ephem.degrees(obs.sidereal_time()-ephem_jupiter.g_ra).norm 238 | ghaj = nadeg(deg) 239 | degj = ephem_jupiter.g_dec 240 | decj = nadeg(degj,2) 241 | 242 | #Saturn 243 | ephem_saturn.compute(Date,epoch=Date) 244 | deg = ephem.degrees(obs.sidereal_time()-ephem_saturn.g_ra).norm 245 | ghasat = nadeg(deg) 246 | degsat = ephem_saturn.g_dec 247 | decsat = nadeg(degsat,2) 248 | 249 | # degv, degmars, degj, degsat have been added for the planetstab function 250 | return ghaa,ghav,decv,ghamars,decmars,ghaj,decj,ghasat,decsat,degv,degmars,degj,degsat 251 | 252 | def vdm_planets(Date): # used in planetstab(m) 253 | # compute v (GHA correction), d (Declination correction), m (magnitude of planet) 254 | 255 | obs = ephem.Observer() 256 | obs.date = Date 257 | 258 | #Venus 259 | obs.date = Date 260 | ephem_venus.compute(Date) 261 | gha = ephem.degrees(obs.sidereal_time()-ephem_venus.g_ra).norm 262 | dec = ephem_venus.g_dec 263 | ephem_venus.compute(Date+ephem.hour) 264 | obs.date = Date+ephem.hour 265 | ghap = ephem.degrees(obs.sidereal_time()-ephem_venus.g_ra).norm 266 | deg = ephem.degrees(ghap-gha).norm-ephem.degrees('15:00:00') 267 | vvenus = "{:0.1f}".format(deg*360*30/pi) 268 | deg = ephem.degrees(ephem_venus.g_dec-dec) 269 | dvenus = "{:0.1f}".format(deg*360*30/pi) 270 | mvenus = "{:0.1f}".format(ephem_venus.mag) 271 | 272 | #Mars 273 | obs.date = Date 274 | ephem_mars.compute(Date) 275 | gha = ephem.degrees(obs.sidereal_time()-ephem_mars.g_ra).norm 276 | dec = ephem_mars.g_dec 277 | ephem_mars.compute(Date+ephem.hour) 278 | obs.date = Date+ephem.hour 279 | ghap = ephem.degrees(obs.sidereal_time()-ephem_mars.g_ra).norm 280 | deg = ephem.degrees(ephem.degrees(ghap-gha).norm-ephem.degrees('15:00:00')) 281 | vmars = "{:0.1f}".format(deg*360*30/pi) 282 | deg = ephem.degrees(ephem_mars.g_dec-dec) 283 | dmars = "{:0.1f}".format(deg*360*30/pi) 284 | mmars = "{:0.1f}".format(ephem_mars.mag) 285 | 286 | #Jupiter 287 | obs.date = Date 288 | ephem_jupiter.compute(Date) 289 | gha = ephem.degrees(obs.sidereal_time()-ephem_jupiter.g_ra).norm 290 | dec = ephem_jupiter.g_dec 291 | ephem_jupiter.compute(Date+ephem.hour) 292 | obs.date = Date+ephem.hour 293 | ghap = ephem.degrees(obs.sidereal_time()-ephem_jupiter.g_ra).norm 294 | deg = ephem.degrees(ephem.degrees(ghap-gha).norm-ephem.degrees('15:00:00')) 295 | vjup = "{:0.1f}".format(deg*360*30/pi) 296 | deg = ephem.degrees(ephem_jupiter.g_dec-dec) 297 | djup = "{:0.1f}".format(deg*360*30/pi) 298 | mjup = "{:0.1f}".format(ephem_jupiter.mag) 299 | 300 | #Saturn 301 | obs.date = Date 302 | ephem_saturn.compute(Date) 303 | gha = ephem.degrees(obs.sidereal_time()-ephem_saturn.g_ra).norm 304 | dec = ephem_saturn.g_dec 305 | ephem_saturn.compute(Date+ephem.hour) 306 | obs.date = Date+ephem.hour 307 | ghap = ephem.degrees(obs.sidereal_time()-ephem_saturn.g_ra).norm 308 | deg = ephem.degrees(ephem.degrees(ghap-gha).norm-ephem.degrees('15:00:00')) 309 | vsat = "{:0.1f}".format(deg*360*30/pi) 310 | deg = ephem.degrees(ephem_saturn.g_dec-dec) 311 | dsat = "{:0.1f}".format(deg*360*30/pi) 312 | msat = "{:0.1f}".format(ephem_saturn.mag) 313 | 314 | return vvenus,dvenus,mvenus,vmars,dmars,mmars,vjup,djup,mjup,vsat,dsat,msat 315 | 316 | #----------------------------------------- 317 | # Aries & planet transit calculations 318 | #----------------------------------------- 319 | 320 | def ariestransit(Date): # used in planetstab(m) 321 | # returns transit time of aries for given date 322 | 323 | obs = ephem.Observer() 324 | obs.date = ephem.date(Date)+1 325 | sid = obs.sidereal_time() 326 | trans = ephem.hours(2*pi-sid/1.00273790935) 327 | # obs.date = Date + trans/(2*pi) #turns ephem.angle (time) into ephem date 328 | hhmm = str(trans)[0:5] # can return "h:mm:" 329 | if hhmm[1:2] == ':': # check if single digit hours 330 | hhmm = '0' + hhmm[0:4] 331 | return hhmm 332 | 333 | def planetstransit(Date, round2seconds = False): # used in starstab 334 | #returns SHA and meridian passage for the navigational planets 335 | 336 | obs = ephem.Observer() 337 | 338 | obs.date = Date 339 | ephem_venus.compute(Date) 340 | vsha = nadeg(2*pi - ephem.degrees(ephem_venus.g_ra).norm) 341 | vtrans = date2time(obs.next_transit(ephem_venus), round2seconds) 342 | hpvenus = "{:0.1f}".format((tan(6371/(ephem_venus.earth_distance*149597870.7)))*60*180/pi) 343 | 344 | obs.date = Date 345 | ephem_mars.compute(Date) 346 | marssha = nadeg(2*pi - ephem.degrees(ephem_mars.g_ra).norm) 347 | marstrans = date2time(obs.next_transit(ephem_mars), round2seconds) 348 | hpmars = "{:0.1f}".format((tan(6371/(ephem_mars.earth_distance*149597870.7)))*60*180/pi) 349 | 350 | obs.date = Date 351 | ephem_jupiter.compute(Date) 352 | jsha = nadeg(2*pi - ephem.degrees(ephem_jupiter.g_ra).norm) 353 | jtrans = date2time(obs.next_transit(ephem_jupiter), round2seconds) 354 | 355 | obs.date = Date 356 | ephem_saturn.compute(Date) 357 | satsha = nadeg(2*pi - ephem.degrees(ephem_saturn.g_ra).norm) 358 | sattrans = date2time(obs.next_transit(ephem_saturn), round2seconds) 359 | 360 | return [vsha,vtrans,marssha,marstrans,jsha,jtrans,satsha,sattrans,hpmars,hpvenus] 361 | 362 | #----------------------- 363 | # star calculations 364 | #----------------------- 365 | 366 | def stellar(Date): # used in starstab 367 | # returns a list of lists with name, SHA and Dec for all navigational stars for epoch of date. 368 | out = [] 369 | for line in db.strip().split('\n'): 370 | st = ephem.readdb(line) 371 | st.compute(Date) # calculate at midnight 372 | out.append([st.name,nadeg(2*pi - ephem.degrees(st.g_ra).norm),nadeg(st.g_dec)]) 373 | return out 374 | 375 | # List of navigational stars with data from Hipparcos, e.g.: 376 | # http://vizier.u-strasbg.fr/viz-bin/VizieR-5?-source=I/311&-out.all&-out.max=10&HIP==677 377 | # The format corresponds to an XEphem database file: 378 | # http://www.clearskyinstitute.com/xephem/help/xephem.html#mozTocId468501 379 | 380 | db = """ 381 | Alpheratz,f|S|B9,0:08:23.26|137.46,29:05:25.55|-163.44,2.04,2000,0 382 | Ankaa,f|S|K0,0:26:17.05|233.05,-42:18:21.55|-356.30,2.55,2000,0 383 | Schedar,f|S|K0,0:40:30.44|50.88,56:32:14.39|-32.13,2.41,2000,0 384 | Diphda,f|S|G9,0:43:35.37|232.55,-17:59:11.78|31.99,2.21,2000,0 385 | Achernar,f|S|B3,1:37:42.85|87.00,-57:14:12.31|-38.24,0.42,2000,0 386 | Hamal,f|S|K2,2:07:10.41|188.55,23:27:44.70|-148.08,2.17,2000,0 387 | Polaris,f|S|F7,2:31:49.09|44.48,89:15:50.79|-11.85,2.11,2000,0 388 | Acamar,f|S|A4,2:58:15.68|-52.89,-40:18:16.85|21.98,2.94,2000,0 389 | Menkar,f|S|M2,3:02:16.77|-10.41,4:05:23.06|-76.85,2.62,2000,0 390 | Mirfak,f|S|F5,3:24:19.37|23.75,49:51:40.25|-26.23,1.90,2000,0 391 | Aldebaran,f|S|K5,4:35:55.24|63.45,16:30:33.49|-188.94,1.00,2000,0 392 | Rigel,f|S|B8,5:14:32.27|1.31,-8:12:05.90|0.50,0.19,2000,0 393 | Capella,f|S|M1,5:16:41.36|75.25,45:59:52.77|-426.89,0.24,2000,0 394 | Bellatrix,f|S|B2,5:25:07.86|-8.11,6:20:58.93|-12.88,1.55,2000,0 395 | Elnath,f|S|B7,5:26:17.51|22.76,28:36:26.83|-173.58,1.62,2000,0 396 | Alnilam,f|S|B0,5:36:12.81|1.44,-1:12:06.91|-0.78,1.62,2000,0 397 | Betelgeuse,f|S|M2,5:55:10.31|27.54,7:24:25.43|11.30,0.50,2000,0 398 | Canopus,f|S|F0,6:23:57.11|19.93,-52:41:44.38|23.24,-0.55,2000,0 399 | Sirius,f|S|A0,6:45:08.92|-546.01,-16:42:58.02|-1223.07,-1.09,2000,0 400 | Adhara,f|S|B2,6:58:37.55|3.24,-28:58:19.51|1.33,1.42,2000,0 401 | Procyon,f|S|F5,7:39:18.12|-714.59,5:13:29.96|-1036.80,0.46,2000,0 402 | Pollux,f|S|K0,7:45:18.95|-626.55,28:01:34.32|-45.80,1.29,2000,0 403 | Avior,f|S|K3,8:22:30.84|-25.52,-59:30:34.14|22.06,2.00,2000,0 404 | Suhail,f|S|K4,9:07:59.76|-24.01,-43:25:57.33|13.52,2.34,2000,0 405 | Miaplacidus,f|S|A2,9:13:11.98|-156.47,-69:43:01.95|108.95,1.66,2000,0 406 | Alphard,f|S|K3,9:27:35.24|-15.23,-8:39:30.96|34.37,2.14,2000,0 407 | Regulus,f|S|B7,10:08:22.31|-248.73,11:58:01.95|5.59,1.32,2000,0 408 | Dubhe,f|S|F7,11:03:43.67|-134.11,61:45:03.72|-34.70,1.95,2000,0 409 | Denebola,f|S|A3,11:49:03.58|-497.68,14:34:19.41|-114.67,2.16,2000,0 410 | Gienah,f|S|B8,12:15:48.37|-158.61,-17:32:30.95|21.86,2.55,2000,0 411 | Acrux,f|S|B0,12:26:35.90|-35.83,-63:05:56.73|-14.86,0.67,2000,0 412 | Gacrux,f|S|M4,12:31:09.96|28.23,-57:06:47.57|-265.08,1.63,2000,0 413 | Alioth,f|S|A0,12:54:01.75|111.91,55:57:35.36|-8.24,1.75,2000,0 414 | Spica,f|S|B1,13:25:11.58|-42.35,-11:09:40.75|-30.67,0.89,2000,0 415 | Alkaid,f|S|B3,13:47:32.44|-121.17,49:18:47.76|-14.91,1.80,2000,0 416 | Hadar,f|S|B1,14:03:49.41|-33.27,-60:22:22.93|-23.16,0.54,2000,0 417 | Menkent,f|S|K0,14:06:40.95|-520.53,-36:22:11.84|-518.06,2.22,2000,0 418 | Arcturus,f|S|K2,14:15:39.67|-1093.39,19:10:56.67|-2000.06,0.11,2000,0 419 | Rigil Kent.,f|S|G2,14:39:36.49|-3679.25,-60:50:02.37|473.67,0.14,2000,0 420 | Kochab,f|S|K4,14:50:42.33|-32.61,74:09:19.81|11.42,2.20,2000,0 421 | Zuben'ubi,f|S|A3,14:50:52.71|-105.68,-16:02:30.40|-68.40,2.79,2000,0 422 | Alphecca,f|S|A0,15:34:41.27|120.27,26:42:52.89|-89.58,2.22,2000,0 423 | Antares,f|S|M1,16:29:24.46|-12.11,-26:25:55.21|-23.30,0.98,2000,0 424 | Atria,f|S|K2,16:48:39.90|17.99,-69:01:39.76|-31.58,2.07,2000,0 425 | Sabik,f|S|A2,17:10:22.69|40.13,-15:43:29.66|99.17,2.44,2000,0 426 | Shaula,f|S|B1,17:33:36.52|-8.53,-37:06:13.76|-30.80,1.52,2000,0 427 | Rasalhague,f|S|A5,17:34:56.07|108.07,12:33:36.13|-221.57,2.13,2000,0 428 | Eltanin,f|S|K5,17:56:36.37|-8.48,51:29:20.02|-22.79,2.36,2000,0 429 | Kaus Aust.,f|S|B9,18:24:10.32|-39.42,-34:23:04.62|-124.20,1.80,2000,0 430 | Vega,f|S|A0,18:36:56.34|200.94,38:47:01.28|286.23,0.09,2000,0 431 | Nunki,f|S|B2,18:55:15.93|15.14,-26:17:48.21|-53.43,2.01,2000,0 432 | Altair,f|S|A7,19:50:47.00|536.23,8:52:05.96|385.29,0.83,2000,0 433 | Peacock,f|S|B2,20:25:38.86|6.90,-56:44:06.32|-86.02,1.86,2000,0 434 | Deneb,f|S|A2,20:41:25.92|2.01,45:16:49.22|1.85,1.30,2000,0 435 | Enif,f|S|K2,21:44:11.16|26.92,9:52:30.03|0.44,2.55,2000,0 436 | Al Na'ir,f|S|B7,22:08:13.98|126.69,-46:57:39.51|-147.47,1.70,2000,0 437 | Fomalhaut,f|S|A3,22:57:39.05|328.95,-29:37:20.05|-164.67,1.18,2000,0 438 | Scheat,f|S|M2,23:03:46.46|187.65,28:04:58.03|136.93,2.49,2000,0 439 | Markab,f|S|B9,23:04:45.65|60.40,15:12:18.96|-41.30,2.48,2000,0 440 | """ 441 | 442 | #-------------------- 443 | # TWILIGHT table 444 | #-------------------- 445 | 446 | # create a list of 'sun above/below horizon' states per Latitude per Normal/Civil/Naut... 447 | #sunvisible = [[None]*3 for i in range(31)] # sunvisible[0][0] up to sunvisible[30][2] 448 | 449 | def twilight(Date, lat, hemisph, round2seconds = False): # used in twilighttab (section 1) 450 | # Returns for given date and latitude(in full degrees): 451 | # naut. and civil twilight (before sunrise), sunrise, meridian passage, sunset, civil and nautical twilight (after sunset). 452 | # NOTE: 'twilight' is only called for every third day in the Full Almanac... 453 | # ...therefore daily tracking of the sun state is impossible. 454 | 455 | mth = ephem.date(Date).triple()[1] 456 | out = [0,0,0,0,0,0,0] 457 | obs = ephem.Observer() 458 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 459 | obs.lat = latitude 460 | 461 | if round2seconds: 462 | d = ephem.date(Date) - 0.5 * ephem.second # search from 0.5 seconds before midnight 463 | else: 464 | d = ephem.date(Date) - 30 * ephem.second # search from 30 seconds before midnight 465 | 466 | obs.date = d 467 | obs.pressure = 0 468 | s = ephem.Sun(obs) 469 | s.compute(d) 470 | r = s.radius 471 | abhd = False # above/below horizon display NOT enabled 472 | 473 | obs.horizon = '-0:34' # 34' (atmospheric refraction) 474 | try: 475 | out[2] = date2time(obs.next_rising(s), round2seconds) # sunrise 476 | except: 477 | out[2] = '--:--' 478 | obs.date = d 479 | try: 480 | out[4] = date2time(obs.next_setting(s), round2seconds) # sunset 481 | except: 482 | out[4] = '--:--' 483 | if out[2] == '--:--' and out[4] == '--:--': # if neither sunrise nor sunset... 484 | abhd = True # enable above/below horizon display 485 | if config.search_next_rising_sun: 486 | yn = getsunstate(Date, lat, 0) # ... get the sun state 487 | else: 488 | yn = midnightsun(mth, hemisph) 489 | out[2] = yn 490 | out[4] = yn 491 | #----------------------------------------------------------- 492 | obs.horizon = ephem.degrees('-6')+r # Civil twilight... 493 | obs.date = d 494 | try: 495 | out[1] = date2time(obs.next_rising(s), round2seconds) # begin 496 | except: 497 | out[1] = '--:--' 498 | obs.date = d 499 | try: 500 | out[5] = date2time(obs.next_setting(s), round2seconds) # end 501 | except: 502 | out[5] = '--:--' 503 | if abhd and out[1] == '--:--' and out[5] == '--:--': # if neither begin nor end... 504 | if config.search_next_rising_sun: 505 | yn = getsunstate(Date, lat, 1) # ... get the sun state 506 | else: 507 | yn = midnightsun(mth, hemisph) 508 | out[1] = yn 509 | out[5] = yn 510 | #----------------------------------------------------------- 511 | obs.horizon = ephem.degrees('-12')+r # Nautical twilight ... 512 | obs.date = d 513 | try: 514 | out[0] = date2time(obs.next_rising(s), round2seconds) # begin 515 | except: 516 | out[0] = '--:--' 517 | obs.date = d 518 | try: 519 | out[6] = date2time(obs.next_setting(s), round2seconds) # end 520 | except: 521 | out[6] = '--:--' 522 | if abhd and out[0] == '--:--' and out[6] == '--:--': # if neither begin nor end... 523 | if config.search_next_rising_sun: 524 | yn = getsunstate(Date, lat, 2) # ... get the sun state 525 | else: 526 | yn = midnightsun(mth, hemisph) 527 | out[0] = yn 528 | out[6] = yn 529 | #----------------------------------------------------------- 530 | obs.date = d 531 | out[3] = date2time(obs.next_transit(s), round2seconds) 532 | 533 | return out 534 | 535 | def getsunstate(d, lat, h): 536 | # populate the sun state (visible or not) for the specified date & latitude 537 | # note: the first parameter 'd' is an ephem date at midnight 538 | # note: getsunstate is called when there is neither a sunrise nor a sunset on 'd' 539 | 540 | i = config.lat.index(lat) 541 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 542 | obs = ephem.Observer() 543 | #d = ephem.date(d) - 30 * ephem.second 544 | obs.pressure = 0 545 | s = ephem.Sun(obs) 546 | err = False 547 | obs.date = d 548 | obs.lat = latitude 549 | s.compute(d) 550 | sunup = False 551 | 552 | if h == 0: 553 | obs.horizon = '-0:34' # sunrise/sunset 554 | if h == 1: 555 | r = s.radius 556 | obs.horizon = ephem.degrees('-6')+r # Civil twilight... 557 | if h == 2: 558 | r = s.radius 559 | obs.horizon = ephem.degrees('-12')+r # Nautical twilight... 560 | 561 | nextrising = d + 100.0 # in case sunset but no next sunrise 562 | nextsetting = d + 100.0 # in case sunrise but no next sunset 563 | 564 | try: 565 | nextrising = obs.next_rising(s) 566 | except ephem.NeverUpError: 567 | err = True 568 | #print("nr NeverUp",i,h,d) 569 | sunup = False 570 | #sunvisible[i][h] = False 571 | except ephem.AlwaysUpError: 572 | err = True 573 | #print("nr AlwaysUp",i,h,d) 574 | sunup = True 575 | #sunvisible[i][h] = True 576 | except Exception: 577 | flag_msg("Oops! sun nextR {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 578 | #sys.exc_clear() # only in Python 2 579 | 580 | obs.date = d 581 | if not(err): # note - 'nextrising' above *should* fail 582 | try: 583 | nextsetting = obs.next_setting(s) 584 | except ephem.NeverUpError: 585 | err = True 586 | #print("ns NeverUp",i,h,d) 587 | sunup = False 588 | #sunvisible[i][h] = False 589 | except ephem.AlwaysUpError: 590 | err = True 591 | #print("ns AlwaysUp",i,h,d) 592 | sunup = True 593 | #sunvisible[i][h] = True 594 | except Exception: 595 | flag_msg("Oops! sun nextS {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 596 | #sys.exc_clear() # only in Python 2 597 | 598 | if not(err): # note - "err == True" *is* expected... 599 | # however if we found both, which occured first? 600 | sunup = False 601 | #sunvisible[i][h] = False 602 | if nextrising > nextsetting: 603 | sunup = True 604 | #sunvisible[i][h] = True 605 | #print("{}".format(i), nextrising, nextsetting, sunvisible[i][h]) 606 | 607 | # return the current sunstate 608 | return formatsun('--:--', sunup, True) 609 | 610 | ##NEW## 611 | def formatsun(t, sunup, ab_enabled): 612 | # return a formatted string of the current sunrise/sunset/twilight time or state 613 | # t = time (hh:mm) or '--:--' 614 | # ab_enabled = True to show time of event or sun event state above/below horizon 615 | # ab_enabled = False to show "event does not happen" as '--:--' 616 | # sunup = True if sun continuously above horizon 617 | # sunup = False if sun continuously below horizon 618 | 619 | if not ab_enabled: # if above/below horizon display enabled 620 | return t 621 | elif sunup: 622 | return r'''\begin{tikzpicture}\draw (0,0) rectangle (12pt,4pt);\end{tikzpicture}''' 623 | else: 624 | return r'''\rule{12Pt}{4Pt}''' 625 | 626 | def midnightsun(mth, hemisph): 627 | # simple way to fudge whether the sun is up or down when there's no 628 | # sunrise or sunset on a day depending on the month and hemisphere only. 629 | # Note: this works for the chosen latitudes to be calculated. 630 | 631 | sunup = False 632 | if mth > 3 and mth < 10: # if April to September inclusive 633 | sunup = True 634 | if hemisph == 'S': 635 | sunup = not(sunup) 636 | return formatsun('--:--', sunup, True) 637 | 638 | #------------------------- 639 | # MOONRISE/-SET table 640 | #------------------------- 641 | 642 | # create a list of 'moon above/below horizon' states per Latitude... 643 | # None = unknown; True = above horizon (visible); False = below horizon (not visible) 644 | # moonvisible[0] is not linked to a latitude but a manual override 645 | moonvisible = [None] * 32 # moonvisible[0] up to moonvisible[31] 646 | 647 | def moonrise_set(Date, lat): # used by tables.py in twilighttab (section 2) 648 | # - - - TIMES ARE ROUNDED TO MINUTES - - - 649 | # returns moonrise and moonset for the given date and latitude plus next 2 days: 650 | # rise day 1, rise day 2, rise day 3, set day 1, set day 2, set day 3 651 | # Additionally it also tracks the current state of the moon (above or below horizon) 652 | 653 | i = 1 + config.lat.index(lat) # index 0 is reserved to enable an explicit setting 654 | out = ['--:--','--:--','--:--','--:--','--:--','--:--'] # first event 655 | out2 = ['--:--','--:--','--:--','--:--','--:--','--:--'] # second event on same day (rare) 656 | 657 | obs = ephem.Observer() 658 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 659 | obs.lat = latitude 660 | obs.pressure = 0 661 | obs.horizon = '-0:34' # 34' (atmospheric refraction) 662 | d = ephem.date(Date) - 30 * ephem.second # search from 30 seconds before midnight 663 | obs.date = d 664 | m = ephem.Moon(obs) 665 | m.compute(d) 666 | #----------------------------------------------------------- 667 | # Moonrise/Moonset on 1st. day ... 668 | try: 669 | firstrising = obs.next_rising(m) 670 | if firstrising-obs.date >= 1: 671 | raise ValueError('event next day') 672 | out[0] = date2time(firstrising) # note: overflow to 00:00 next day is correct here 673 | lastevent = firstrising 674 | moonvisible[i] = True 675 | except Exception: # includes NeverUpError and AlwaysUpError 676 | out[0] = '--:--' 677 | lastevent = 0 678 | 679 | if out[0] != '--:--': 680 | try: 681 | nextr = obs.next_rising(m, start=firstrising) 682 | if nextr-obs.date < 1: 683 | out2[0] = date2time(nextr) # note: overflow to 00:00 next day is correct here 684 | lastevent = nextr 685 | except UnboundLocalError: 686 | pass 687 | except ephem.NeverUpError: 688 | pass 689 | except ephem.AlwaysUpError: 690 | pass 691 | except Exception: 692 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 693 | #sys.exc_clear() # only in Python 2 694 | 695 | obs.date = d 696 | try: 697 | firstsetting = obs.next_setting(m) 698 | if firstsetting-obs.date >= 1: 699 | raise ValueError('event next day') 700 | out[3] = date2time(firstsetting) # note: overflow to 00:00 next day is correct here 701 | if firstsetting > lastevent: 702 | lastevent = firstsetting 703 | moonvisible[i] = False 704 | except Exception: # includes NeverUpError and AlwaysUpError 705 | out[3] = '--:--' 706 | 707 | if out[3] != '--:--': 708 | try: 709 | nexts = obs.next_setting(m, start=firstsetting) 710 | if nexts-obs.date < 1: 711 | out2[3] = date2time(nexts) # note: overflow to 00:00 next day is correct here 712 | if nexts > lastevent: 713 | moonvisible[i] = False 714 | except UnboundLocalError: 715 | pass 716 | except ephem.NeverUpError: 717 | pass 718 | except ephem.AlwaysUpError: 719 | pass 720 | except Exception: 721 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 722 | #sys.exc_clear() # only in Python 2 723 | 724 | if out[0] == '--:--' and out[3] == '--:--': # if neither moonrise nor moonset... 725 | if moonvisible[i] == None: 726 | getmoonstate(d, lat) # ...get moon state if unknown 727 | out[0] = moonstate(i) 728 | out[3] = moonstate(i) 729 | 730 | if out[0] == '--:--' and out[3] != '--:--': # if moonset but no moonrise... 731 | out[0] = moonset_no_rise(d, Date, i, lat) 732 | 733 | if out[0] != '--:--' and out[3] == '--:--': # if moonrise but no moonset... 734 | out[3] = moonrise_no_set(d, Date, i, lat) 735 | 736 | #----------------------------------------------------------- 737 | # Moonrise/Moonset on 2nd. day ... 738 | d = ephem.date(Date + 1) - 30 * ephem.second 739 | obs.date = d 740 | m.compute(d) 741 | try: 742 | firstrising = obs.next_rising(m) 743 | if firstrising-obs.date >= 1: 744 | raise ValueError('event next day') 745 | out[1] = date2time(firstrising) # note: overflow to 00:00 next day is correct here 746 | lastevent = firstrising 747 | moonvisible[i] = True 748 | except Exception: # includes NeverUpError and AlwaysUpError 749 | out[1] = '--:--' 750 | lastevent = 0 751 | 752 | if out[1] != '--:--': 753 | try: 754 | nextr = obs.next_rising(m, start=firstrising) 755 | if nextr-obs.date < 1: 756 | out2[1] = date2time(nextr) # note: overflow to 00:00 next day is correct here 757 | lastevent = nextr 758 | except UnboundLocalError: 759 | pass 760 | except ephem.NeverUpError: 761 | pass 762 | except ephem.AlwaysUpError: 763 | pass 764 | except Exception: 765 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 766 | #sys.exc_clear() # only in Python 2 767 | 768 | obs.date = d 769 | try: 770 | firstsetting = obs.next_setting(m) 771 | if firstsetting-obs.date >= 1: 772 | raise ValueError('event next day') 773 | out[4] = date2time(firstsetting) # note: overflow to 00:00 next day is correct here 774 | if firstsetting > lastevent: 775 | lastevent = firstsetting 776 | moonvisible[i] = False 777 | except Exception: # includes NeverUpError and AlwaysUpError 778 | out[4] = '--:--' 779 | 780 | if out[4] != '--:--': 781 | try: 782 | nexts = obs.next_setting(m, start=firstsetting) 783 | if nexts-obs.date < 1: 784 | out2[4] = date2time(nexts) # note: overflow to 00:00 next day is correct here 785 | if nexts > lastevent: 786 | moonvisible[i] = False 787 | except UnboundLocalError: 788 | pass 789 | except ephem.NeverUpError: 790 | pass 791 | except ephem.AlwaysUpError: 792 | pass 793 | except Exception: 794 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 795 | #sys.exc_clear() # only in Python 2 796 | 797 | if out[1] == '--:--' and out[4] == '--:--': # if neither moonrise nor moonset... 798 | if moonvisible[i] == None: 799 | getmoonstate(d, lat) # ...get moon state if unknown 800 | out[1] = moonstate(i) 801 | out[4] = moonstate(i) 802 | 803 | if out[1] == '--:--' and out[4] != '--:--': # if moonset but no moonrise... 804 | out[1] = moonset_no_rise(d, Date+1, i, lat) 805 | 806 | if out[1] != '--:--' and out[4] == '--:--': # if moonrise but no moonset... 807 | out[4] = moonrise_no_set(d, Date+1, i, lat) 808 | 809 | #----------------------------------------------------------- 810 | # Moonrise/Moonset on 3rd. day ... 811 | d = ephem.date(Date + 2) - 30 * ephem.second 812 | obs.date = d 813 | m.compute(d) 814 | try: 815 | firstrising = obs.next_rising(m) 816 | if firstrising-obs.date >= 1: 817 | raise ValueError('event next day') 818 | out[2] = date2time(firstrising) # note: overflow to 00:00 next day is correct here 819 | lastevent = firstrising 820 | moonvisible[i] = True 821 | except Exception: # includes NeverUpError and AlwaysUpError 822 | out[2] = '--:--' 823 | lastevent = 0 824 | 825 | if out[2] != '--:--': 826 | try: 827 | nextr = obs.next_rising(m, start=firstrising) 828 | if nextr-obs.date < 1: 829 | out2[2] = date2time(nextr) # note: overflow to 00:00 next day is correct here 830 | lastevent = nextr 831 | except UnboundLocalError: 832 | pass 833 | except ephem.NeverUpError: 834 | pass 835 | except ephem.AlwaysUpError: 836 | pass 837 | except Exception: 838 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 839 | #sys.exc_clear() # only in Python 2 840 | 841 | obs.date = d 842 | try: 843 | firstsetting = obs.next_setting(m) 844 | if firstsetting-obs.date >= 1: 845 | raise ValueError('event next day') 846 | out[5] = date2time(firstsetting) # note: overflow to 00:00 next day is correct here 847 | if firstsetting > lastevent: 848 | lastevent = firstsetting 849 | moonvisible[i] = False 850 | except Exception: # includes NeverUpError and AlwaysUpError 851 | out[5] = '--:--' 852 | 853 | if out[5] != '--:--': 854 | try: 855 | nexts = obs.next_setting(m, start=firstsetting) 856 | if nexts-obs.date < 1: 857 | out2[5] = date2time(nexts) # note: overflow to 00:00 next day is correct here 858 | if nexts > lastevent: 859 | moonvisible[i] = False 860 | except UnboundLocalError: 861 | pass 862 | except ephem.NeverUpError: 863 | pass 864 | except ephem.AlwaysUpError: 865 | pass 866 | except Exception: 867 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 868 | #sys.exc_clear() # only in Python 2 869 | 870 | if out[2] == '--:--' and out[5] == '--:--': # if neither moonrise nor moonset... 871 | if moonvisible[i] == None: 872 | getmoonstate(d, lat) # ...get moon state if unknown 873 | out[2] = moonstate(i) 874 | out[5] = moonstate(i) 875 | 876 | if out[2] == '--:--' and out[5] != '--:--': # if moonset but no moonrise... 877 | out[2] = moonset_no_rise(d, Date+2, i, lat) 878 | 879 | if out[2] != '--:--' and out[5] == '--:--': # if moonrise but no moonset... 880 | out[5] = moonrise_no_set(d, Date+2, i, lat) 881 | 882 | return out, out2 883 | 884 | def moonstate(ndx): 885 | # return the current moonstate (if known) 886 | out = '--:--' 887 | if moonvisible[ndx] == True: 888 | #out = 'UP' 889 | #out = r'\framebox(12,4){}' 890 | #out = r'{\setlength{\fboxrule}{0.8pt}\setlength{\fboxsep}{0pt}\fbox{\makebox(12,4){}}}' 891 | #out = r'{\setlength{\fboxrule}{0.8pt}\fbox{\parbox[c][0pt]{0pt}{ }}}' 892 | #out = r'\includegraphics[scale=1.0]{./moonup.jpg}' 893 | out = r'\begin{tikzpicture}\draw (0,0) rectangle (12pt,4pt);\end{tikzpicture}' 894 | if moonvisible[ndx] == False: 895 | #out = 'DOWN' 896 | out = r'\rule{12Pt}{4Pt}' 897 | return out 898 | 899 | def getmoonstate(d, lat): 900 | # populate the moon state (visible or not) for the specified date & latitude 901 | # note: the first parameter 'd' is already an ephem date 30 seconds before midnight 902 | # note: getmoonstate is called when there is neither a moonrise nor a moonset on 'd' 903 | 904 | i = 1 + config.lat.index(lat) # index 0 is reserved to enable an explicit setting 905 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 906 | obs = ephem.Observer() 907 | #d = ephem.date(d) - 30 * ephem.second 908 | obs.pressure = 0 909 | obs.horizon = '-0:34' 910 | m = ephem.Moon(obs) 911 | err = False 912 | obs.date = d 913 | obs.lat = latitude 914 | m.compute(d) 915 | nextrising = d + 100.0 # in case moonset but no next moonrise 916 | nextsetting = d + 100.0 # in case moonrise but no next moonset 917 | 918 | try: 919 | nextrising = obs.next_rising(m) 920 | except ephem.NeverUpError: 921 | err = True 922 | #print("nr NeverUp") 923 | moonvisible[i] = False 924 | except ephem.AlwaysUpError: 925 | err = True 926 | #print("nr AlwaysUp") 927 | moonvisible[i] = True 928 | except Exception: 929 | flag_msg("Oops! moon nextR {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 930 | #sys.exc_clear() # only in Python 2 931 | 932 | obs.date = d 933 | if not(err): # note - 'nextrising' above *should* fail 934 | try: 935 | nextsetting = obs.next_setting(m) 936 | except ephem.NeverUpError: 937 | err = True 938 | #print("ns NeverUp") 939 | moonvisible[i] = False 940 | except ephem.AlwaysUpError: 941 | err = True 942 | #print("ns AlwaysUp") 943 | moonvisible[i] = True 944 | except Exception: 945 | flag_msg("Oops! moon nextS {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 946 | #sys.exc_clear() # only in Python 2 947 | 948 | if not(err): # note - "err == True" *is* expected... 949 | # however if we found both, which occured first? 950 | moonvisible[i] = False 951 | if nextrising > nextsetting: 952 | moonvisible[i] = True 953 | #print("{}".format(i), nextrising, nextsetting, moonvisible[i]) 954 | return 955 | 956 | ##NEW## 957 | def moonset_no_rise(d, Date, i, lat): 958 | # if moonset but no moonrise... 959 | msg = "" 960 | n = seek_moonrise(d, lat) 961 | if n == 1: 962 | out = moonstate(i) # moonrise "below horizon" 963 | msg = "below horizon (start)" 964 | if n == -1: 965 | #print("UP") 966 | moonvisible[0] = True 967 | out = moonstate(0) # moonrise "above horizon" 968 | msg = "above horizon (end)" 969 | #print(out[0]) 970 | #if msg != "": 971 | #print("no moonrise on {} at lat {} => {}".format(ephem.date(Date).datetime().strftime("%Y-%m-%d"), lat, msg)) 972 | if n == 0: 973 | out = r'''\raisebox{0.24ex}{\boldmath$\cdot\cdot$~\boldmath$\cdot\cdot$}''' 974 | return out 975 | 976 | ##NEW## 977 | def moonrise_no_set(d, Date, i, lat): 978 | # if moonrise but no moonset... 979 | msg = "" 980 | n = seek_moonset(d, lat) 981 | if n == 1: 982 | out = moonstate(i) # moonset "above horizon" 983 | msg = "above horizon (start)" 984 | if n == -1: 985 | moonvisible[0] = False 986 | out = moonstate(0) # moonset "below horizon" 987 | msg = "below horizon (end)" 988 | #if msg != "": 989 | #print("no moonset on {} at lat {} => {}".format(ephem.date(Date).datetime().strftime("%Y-%m-%d"), lat, msg)) 990 | if n == 0: 991 | out = r'''\raisebox{0.24ex}{\boldmath$\cdot\cdot$~\boldmath$\cdot\cdot$}''' 992 | return out 993 | 994 | ##NEW## 995 | def seek_moonset(d, lat): 996 | # for the specified date & latitude ... 997 | # return -1 if there is NO MOONSET yesterday 998 | # return +1 if there is NO MOONSET tomorrow 999 | # return 0 if there was a moonset yesterday and will be a moonset tomorrow 1000 | # note: this is called when there is only a moonrise on the specified date+latitude 1001 | 1002 | m_set_t = 0 # normal case: assume moonsets yesterday & tomorrow 1003 | 1004 | i = 1 + config.lat.index(lat) # index 0 is reserved to enable an explicit setting 1005 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 1006 | obs = ephem.Observer() 1007 | #d = ephem.date(d) - 30 * ephem.second 1008 | obs.pressure = 0 # turn off PyEphem’s native mechanism for computing atmospheric refraction near the horizon 1009 | obs.horizon = '-0:34' 1010 | m = ephem.Moon(obs) 1011 | err = False 1012 | obs.date = d 1013 | obs.lat = latitude 1014 | m.compute(d) 1015 | nextsetting = d + 10.0 # in case moonrise but no next moonset 1016 | 1017 | try: 1018 | nextsetting = obs.next_setting(m) 1019 | except ephem.NeverUpError: 1020 | err = True 1021 | #print("ns NeverUp") 1022 | flag_msg("Oops! moon nextS {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1023 | except ephem.AlwaysUpError: 1024 | err = True 1025 | m_set_t = +1 1026 | #print("ns AlwaysUp") 1027 | except Exception: 1028 | flag_msg("Oops! moon nextS {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1029 | #sys.exc_clear() # only in Python 2 1030 | 1031 | if not(err): # note - "err == True" *is* expected... 1032 | # moonset detected - is it after tomorrow? 1033 | if nextsetting > d + 2.0: 1034 | m_set_t = +1 1035 | 1036 | obs.date = d 1037 | if m_set_t == 0: 1038 | try: 1039 | prevsetting = obs.previous_setting(m) 1040 | except ephem.NeverUpError: 1041 | err = True 1042 | m_set_t = -1 1043 | #print("ps NeverUp") 1044 | except ephem.AlwaysUpError: 1045 | err = True 1046 | #print("ps AlwaysUp") 1047 | flag_msg("Oops! moon prevS {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1048 | except Exception: 1049 | flag_msg("Oops! moon prevS {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1050 | #sys.exc_clear() # only in Python 2 1051 | 1052 | if not(err): # note - "err == True" *is* expected... 1053 | # moonset detected - is it before yesterday? 1054 | if prevsetting < d - 1.0: 1055 | m_set_t = -1 1056 | #print("m_set_t = {}".format(m_set_t)) 1057 | return m_set_t 1058 | 1059 | ##NEW## 1060 | def seek_moonrise(d, lat): 1061 | # return -1 if there is NO MOONRISE yesterday 1062 | # return +1 if there is NO MOONRISE tomorrow 1063 | # return 0 if there was a moonrise yesterday and will be a moonrise tomorrow 1064 | # note: this is called when there is only a moonset on the specified date+latitude 1065 | 1066 | m_rise_t = 0 # normal case: assume moonrise yesteray & tomorrow 1067 | 1068 | i = 1 + config.lat.index(lat) # index 0 is reserved to enable an explicit setting 1069 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 1070 | obs = ephem.Observer() 1071 | #d = ephem.date(d) - 30 * ephem.second 1072 | obs.pressure = 0 1073 | obs.horizon = '-0:34' 1074 | m = ephem.Moon(obs) 1075 | err = False 1076 | obs.date = d 1077 | obs.lat = latitude 1078 | m.compute(d) 1079 | nextrising = d + 10.0 # in case moonset but no next moonrise 1080 | 1081 | try: 1082 | nextrising = obs.next_rising(m) 1083 | except ephem.NeverUpError: 1084 | err = True 1085 | m_rise_t = +1 1086 | #print("nr NeverUp") 1087 | except ephem.AlwaysUpError: 1088 | err = True 1089 | #print("nr AlwaysUp") 1090 | flag_msg("Oops! moon nextR {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1091 | except Exception: 1092 | flag_msg("Oops! moon nextR {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1093 | #sys.exc_clear() # only in Python 2 1094 | 1095 | if not(err): # note - "err == True" *is* expected... 1096 | # moonrise detected - is it after tomorrow? 1097 | if nextrising > d + 2.0: 1098 | m_rise_t = +1 1099 | 1100 | obs.date = d 1101 | if m_rise_t == 0: 1102 | try: 1103 | prevrising = obs.previous_rising(m) 1104 | except ephem.NeverUpError: 1105 | err = True 1106 | #print("pr NeverUp") 1107 | flag_msg("Oops! moon prevR {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1108 | except ephem.AlwaysUpError: 1109 | err = True 1110 | m_rise_t = -1 1111 | #print("pr AlwaysUp") 1112 | except Exception: 1113 | flag_msg("Oops! moon prevR {}: {} occured, line: {}".format(i,sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1114 | #sys.exc_clear() # only in Python 2 1115 | 1116 | if not(err): # note - "err == True" *is* expected... 1117 | # moonrise detected - is it before yesterday? 1118 | if prevrising < d - 1.0: 1119 | m_rise_t = -1 1120 | 1121 | #print("m_rise_t = {}".format(m_rise_t)) 1122 | return m_rise_t 1123 | 1124 | #------------------------- 1125 | # EVENT TIME tables 1126 | #------------------------- 1127 | 1128 | def moonrise_set2(Date, lat): # used in twilighttab of eventtables.py 1129 | # - - - TIMES ARE ROUNDED TO SECONDS - - - 1130 | # returns moonrise and moonset for the given date and latitude: 1131 | # rise time, set time 1132 | # Additionally it also tracks the current state of the moon (above or below horizon) 1133 | 1134 | i = 1 + config.lat.index(lat) # index 0 is reserved to enable an explicit setting 1135 | out = ['--:--','--:--'] # first event 1136 | out2 = ['--:--','--:--'] # second event on same day (rare) 1137 | 1138 | obs = ephem.Observer() 1139 | latitude = ephem.degrees('{}:00:00.0'.format(lat)) 1140 | obs.lat = latitude 1141 | obs.pressure = 0 1142 | obs.horizon = '-0:34' # 34' (atmospheric refraction) 1143 | d = ephem.date(Date) - 0.5 * ephem.second # search from 0.5 seconds before midnight 1144 | obs.date = d 1145 | m = ephem.Moon(obs) 1146 | m.compute(d) 1147 | #----------------------------------------------------------- 1148 | # Moonrise/Moonset on the selected day ... 1149 | try: 1150 | firstrising = obs.next_rising(m) 1151 | if firstrising-obs.date >= 1: 1152 | raise ValueError('event next day') 1153 | out[0] = hhmmss(firstrising) # note: overflow to 00:00 next day is correct here 1154 | lastevent = firstrising 1155 | moonvisible[i] = True 1156 | except Exception: # includes NeverUpError and AlwaysUpError 1157 | out[0] = '--:--' 1158 | lastevent = 0 1159 | 1160 | if out[0] != '--:--': 1161 | try: 1162 | nextr = obs.next_rising(m, start=firstrising) 1163 | if nextr-obs.date < 1: 1164 | out2[0] = hhmmss(nextr) # note: overflow to 00:00 next day is correct here 1165 | lastevent = nextr 1166 | except UnboundLocalError: 1167 | pass 1168 | except ephem.NeverUpError: 1169 | pass 1170 | except ephem.AlwaysUpError: 1171 | pass 1172 | except Exception: 1173 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1174 | #sys.exc_clear() # only in Python 2 1175 | 1176 | obs.date = d 1177 | try: 1178 | firstsetting = obs.next_setting(m) 1179 | if firstsetting-obs.date >= 1: 1180 | raise ValueError('event next day') 1181 | out[1] = hhmmss(firstsetting) # note: overflow to 00:00 next day is correct here 1182 | if firstsetting > lastevent: 1183 | lastevent = firstsetting 1184 | moonvisible[i] = False 1185 | except Exception: # includes NeverUpError and AlwaysUpError 1186 | out[1] = '--:--' 1187 | 1188 | if out[1] != '--:--': 1189 | try: 1190 | nexts = obs.next_setting(m, start=firstsetting) 1191 | if nexts-obs.date < 1: 1192 | out2[1] = hhmmss(nexts) # note: overflow to 00:00 next day is correct here 1193 | if nexts > lastevent: 1194 | moonvisible[i] = False 1195 | except UnboundLocalError: 1196 | pass 1197 | except ephem.NeverUpError: 1198 | pass 1199 | except ephem.AlwaysUpError: 1200 | pass 1201 | except Exception: 1202 | flag_msg("Oops! {} occured, line: {}".format(sys.exc_info()[1],sys.exc_info()[2].tb_lineno)) 1203 | #sys.exc_clear() # only in Python 2 1204 | 1205 | if out[0] == '--:--' and out[1] == '--:--': # if neither moonrise nor moonset... 1206 | if moonvisible[i] == None: 1207 | getmoonstate(d, lat) # ...get moon state if unknown 1208 | out[0] = moonstate(i) 1209 | out[1] = moonstate(i) 1210 | 1211 | if out[0] == '--:--' and out[1] != '--:--': # if moonset but no moonrise... 1212 | out[0] = moonset_no_rise(d, Date, i, lat) 1213 | 1214 | if out[0] != '--:--' and out[1] == '--:--': # if moonrise but no moonset... 1215 | out[1] = moonrise_no_set(d, Date, i, lat) 1216 | 1217 | return out, out2 1218 | 1219 | #------------------------------ 1220 | # Equation of Time section 1221 | #------------------------------ 1222 | 1223 | def equation_of_time(Date, round2seconds = False): # used in twilighttab (section 3) 1224 | # returns equation of time, the sun's transit time, 1225 | # the moon's transit-, antitransit-time, age and percent illumination. 1226 | # (Equation of Time = Mean solar time - Apparent solar time) 1227 | 1228 | py_date = Date.tuple() 1229 | py_obsdate = date(py_date[0], py_date[1], py_date[2]) 1230 | 1231 | if round2seconds: 1232 | # !! transit times are rounded to the nearest second, 1233 | # !! so the search needs to start and end 0.5 sec earlier 1234 | # !! e.g. after 23h 59m 59.5s rounds up to 00:00:00 next day 1235 | d = ephem.date(Date) - 0.5 * ephem.second 1236 | else: 1237 | # !! transit times are rounded to the nearest minute, 1238 | # !! so the search needs to start and end 30 sec earlier 1239 | # !! e.g. after 23h 59m 30s rounds up to 00:00 next day 1240 | d = ephem.date(Date) - 30 * ephem.second 1241 | 1242 | obs = ephem.Observer() 1243 | obs.date = d 1244 | ephem_sun.compute(d) 1245 | ephem_moon.compute(d) 1246 | transs = '--:--' 1247 | antim = '--:--' 1248 | transm = '--:--' 1249 | 1250 | next_s_tr = obs.next_transit(ephem_sun,start=d) 1251 | if next_s_tr - obs.date < 1: 1252 | transs = date2time(next_s_tr, round2seconds) 1253 | 1254 | next_m_atr = obs.next_antitransit(ephem_moon,start=d) 1255 | if next_m_atr - obs.date < 1: 1256 | antim = date2time(next_m_atr, round2seconds) 1257 | 1258 | next_m_tr = obs.next_transit(ephem_moon,start=d) 1259 | if next_m_tr - obs.date < 1: 1260 | transm = date2time(next_m_tr, round2seconds) 1261 | 1262 | #----------------------------- 1263 | obs = ephem.Observer() 1264 | obs.date = Date 1265 | 1266 | ephem_moon.compute(Date) 1267 | pct = int(round(ephem_moon.phase)) # percent of moon surface illuminated 1268 | age = int(round((Date+0.5)-ephem.previous_new_moon(Date+0.5))) 1269 | phase = ephem_moon.elong.norm+0.0 # moon phase as float (0:new to π:full to 2π:new) 1270 | 1271 | ephem_sun.compute(Date-0.1) 1272 | obs.date = Date-0.1 1273 | 1274 | # round to the second; convert back to days 1275 | x = round((obs.next_antitransit(ephem_sun)-Date)*86400)*2*pi/86400 1276 | eqt00 = ephem.hours(x) 1277 | eqt00 = str(eqt00)[-8:-3] 1278 | if x >= 0: 1279 | eqt00 = r"\colorbox{{lightgray!60}}{{{}}}".format(eqt00) 1280 | 1281 | y = round((obs.next_transit(ephem_sun)-(Date+0.5))*86400)*2*pi/86400 1282 | eqt12 = ephem.hours(y) 1283 | eqt12 = str(eqt12)[-8:-3] 1284 | if y >= 0: 1285 | eqt12 = r"\colorbox{{lightgray!60}}{{{}}}".format(eqt12) 1286 | 1287 | return eqt00,eqt12,transs,transm,antim,age,pct -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | # Copyright (C) 2022 Andrew Bauer 5 | 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 2 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program; if not, write to the Free Software Foundation, Inc., 18 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 19 | 20 | # ================ EDIT LINES IN THIS SECTION ONLY ================ 21 | 22 | pgsz = 'A4' # page size 'A4' or 'Letter' (global variable) 23 | search_next_rising_sun = False # 'False' = base it only on month and hemisphere 24 | 25 | # ================ DO NOT EDIT LINES BELOW HERE ================ 26 | # Docker-related stuff... 27 | dockerized = False # 'True' to build this app to run in a Docker-Linux container 28 | # NOTE: config.py has been "Dockerized" by use of environment variables in .env 29 | 30 | # Docker Container subfolder for creating PDF files (and optionally a LOG file) 31 | # This folder must be mapped to a Named Volume as part of the 'docker run' command: 32 | # e.g. -v "%cd%\pdf":/app/tmp in a Windows host system 33 | # e.g. -v $(pwd)/pdf:/app/tmp in a macOS/Linux host system 34 | docker_pdf = "tmp" 35 | docker_prefix = docker_pdf + "/" if dockerized else "" # docker image is based on Linux 36 | docker_postfix = "/" + docker_pdf if dockerized else "" # docker image is based on Linux 37 | # ============================================================== 38 | 39 | # global variables initialized during main program startup 40 | WINpf = False # system platform 41 | LINUXpf = False # system platform 42 | MACOSpf = False # system platform 43 | FANCYhd = False # 'True' if compatible with 'fancyhdr' package 44 | DPonly = False # output data pages only 45 | 46 | # define global variables 47 | logfileopen = False 48 | tbls = '' # table style (global variable) 49 | decf = '' # Declination format (global variable) 50 | 51 | # list of latitudes to include for Sunrise/Sunset/Twilight/Moonrise/Moonset... 52 | lat = [72,70,68,66,64,62,60,58,56,54,52,50,45,40,35,30,20,10,0,-10,-20,-30,-35,-40,-45,-50,-52,-54,-56,-58,-60] 53 | 54 | # open/write/close a log file 55 | def initLOG(): 56 | global errors 57 | errors = 0 58 | global logfile 59 | logfile = open(docker_prefix + 'debug.log', mode="w", encoding="utf8") 60 | global logfileopen 61 | logfileopen = True 62 | 63 | # write to log file 64 | def writeLOG(text): 65 | logfile.write(text) 66 | return 67 | 68 | # close log file 69 | def closeLOG(): 70 | logfileopen = False 71 | logfile.close() 72 | return 73 | -------------------------------------------------------------------------------- /eventtables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2022 Andrew Bauer 5 | 6 | # This program is free software; you can redistribute it and/or modify 7 | # it under the terms of the GNU General Public License as published by 8 | # the Free Software Foundation; either version 3 of the License, or 9 | # (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along 17 | # with this program. If not, see . 18 | 19 | # NOTE: the new format statement requires a literal '{' to be entered as '{{', 20 | # and a literal '}' to be entered as '}}'. The old '%' format specifier 21 | # will be removed from Python at some later time. See: 22 | # https://docs.python.org/3/whatsnew/3.0.html#pep-3101-a-new-approach-to-string-formatting 23 | 24 | ###### Standard library imports ###### 25 | from datetime import datetime, timedelta 26 | 27 | ###### Third party imports ###### 28 | import ephem 29 | 30 | ###### Local application imports ###### 31 | from alma_ephem import * 32 | import config 33 | 34 | 35 | #---------------------- 36 | # internal methods 37 | #---------------------- 38 | 39 | def fmtdate(d): 40 | if config.pgsz == 'Letter': return d.strftime("%m/%d/%Y") 41 | return d.strftime("%d.%m.%Y") 42 | 43 | def fmtdates(d1,d2): 44 | if config.pgsz == 'Letter': return d1.strftime("%m/%d/%Y") + " - " + d2.strftime("%m/%d/%Y") 45 | return d1.strftime("%d.%m.%Y") + " - " + d2.strftime("%d.%m.%Y") 46 | 47 | def double_events_found(m1, m2): 48 | # check for two moonrise/moonset events on the same day & latitude 49 | dbl = False 50 | for i in range(len(m1)): 51 | if m2[i] != '--:--': 52 | dbl = True 53 | return dbl 54 | 55 | # >>>>>>>>>>>>>>>>>>>>>>>> 56 | def twilighttab(date): 57 | # returns the twilight and moonrise tables 58 | 59 | first_day = r'''{}/{}/{}'''.format(date.year,date.month,date.day) 60 | dfl = ephem.Date(first_day) # convert date to float 61 | 62 | # Twilight tables ........................................... 63 | #lat = [72,70,68,66,64,62,60,58,56,54,52,50,45,40,35,30,20,10,0, -10,-20,-30,-35,-40,-45,-50,-52,-54,-56,-58,-60] 64 | latNS = [72, 70, 58, 40, 10, -10, -50, -60] 65 | # tab = r'''\begin{tabular*}{0.72\textwidth}[t]{@{\extracolsep{\fill}}|r|ccc|ccc|cc|} 66 | tab = r'''\begin{tabular}[t]{|r|ccc|ccc|cc|} 67 | %%%\multicolumn{9}{c}{\normalsize{}}\\ 68 | ''' 69 | 70 | ondate = ephem.date(dfl).datetime().strftime("%d %B %Y") 71 | tab = tab + r'''\hline 72 | \multicolumn{{9}}{{|c|}}{{\rule{{0pt}}{{2.4ex}}{{\textbf{{{}}}}}}}\\ 73 | '''.format(ondate) 74 | 75 | tab = tab + r'''\hline 76 | \multicolumn{1}{|c|}{\rule{0pt}{2.4ex}\multirow{2}{*}{\textbf{Lat.}}} & 77 | \multicolumn{2}{c}{\textbf{Twilight}} & 78 | \multicolumn{1}{|c|}{\multirow{2}{*}{\textbf{Sunrise}}} & 79 | \multicolumn{1}{c|}{\multirow{2}{*}{\textbf{Sunset}}} & 80 | \multicolumn{2}{c|}{\textbf{Twilight}} & 81 | \multicolumn{1}{c|}{\multirow{2}{*}{\textbf{Moonrise}}} & 82 | \multicolumn{1}{c|}{\multirow{2}{*}{\textbf{Moonset}}}\\ 83 | \multicolumn{1}{|c|}{} & 84 | \multicolumn{1}{c}{Naut.} & 85 | \multicolumn{1}{c}{Civil} & 86 | \multicolumn{1}{|c|}{} & 87 | \multicolumn{1}{c|}{} & 88 | \multicolumn{1}{c}{Civil} & 89 | \multicolumn{1}{c|}{Naut.} & 90 | \multicolumn{1}{c|}{} & 91 | \multicolumn{1}{c|}{}\\ 92 | \hline\rule{0pt}{2.6ex}\noindent 93 | ''' 94 | lasthemisph = "" 95 | j = 5 96 | for i in config.lat: 97 | if i >= 0: 98 | hemisph = 'N' 99 | else: 100 | hemisph = 'S' 101 | if not(i in latNS): 102 | hs = "" 103 | else: 104 | hs = hemisph 105 | if j%6 == 0: 106 | tab = tab + r'''\rule{0pt}{2.6ex} 107 | ''' 108 | lasthemisph = hemisph 109 | twi = twilight(dfl, i, hemisph, True) # True = round to seconds 110 | moon, moon2 = moonrise_set2(dfl,i) 111 | if not(double_events_found(moon,moon2)): 112 | line = r'''\textbf{{{}}}'''.format(hs) + r''' {}$^\circ$'''.format(abs(i)) 113 | line = line + r''' & {} & {} & {} & {} & {} & {} & {} & {} \\ 114 | '''.format(twi[0],twi[1],twi[2],twi[4],twi[5],twi[6],moon[0],moon[1]) 115 | else: 116 | # print a row with two moonrise/moonset events on the same day & latitude 117 | line = r'''\multirow{{2}}{{*}}{{\textbf{{{}}} {}$^\circ$}}'''.format(hs,abs(i)) 118 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(twi[0]) 119 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(twi[1]) 120 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(twi[2]) 121 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(twi[4]) 122 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(twi[5]) 123 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(twi[6]) 124 | 125 | # top row... 126 | for k in range(len(moon)): 127 | if moon2[k] != '--:--': 128 | line = line + r''' & \colorbox{{khaki!45}}{{{}}}'''.format(moon[k]) 129 | else: 130 | line = line + r''' & \multirow{{2}}{{*}}{{{}}}'''.format(moon[k]) 131 | line = line + r'''\\ 132 | ''' # terminate top row 133 | # bottom row... 134 | line = line + r'''& & & & & & ''' 135 | for k in range(len(moon)): 136 | if moon2[k] != '--:--': 137 | line = line + r''' & \colorbox{{khaki!45}}{{{}}}'''.format(moon2[k]) 138 | else: 139 | line = line + r'''&''' 140 | line = line + r''' \\ 141 | ''' # terminate bottom row 142 | 143 | tab = tab + line 144 | j += 1 145 | # add space between tables... 146 | tab = tab + r'''\hline\multicolumn{9}{c}{}\\ 147 | ''' 148 | tab = tab + r'''\end{tabular} 149 | ''' 150 | return tab 151 | 152 | # >>>>>>>>>>>>>>>>>>>>>>>> 153 | def meridiantab(date): 154 | # returns a table with ephemerides for the navigational stars 155 | 156 | first_day = r'''{}/{}/{}'''.format(date.year,date.month,date.day) 157 | dfl = ephem.Date(first_day) # convert date to float 158 | 159 | # LaTeX SPACING: \enskip \quad \qquad 160 | out = r'''\quad 161 | \begin{tabular*}{0.25\textwidth}[t]{@{\extracolsep{\fill}}|rrr|} 162 | %%%\multicolumn{3}{c}{\normalsize{}}\\ 163 | ''' 164 | m = "" 165 | # returns a table with SHA & Mer.pass for Venus, Mars, Jupiter and Saturn 166 | dt = ephem.date(dfl).datetime() 167 | datestr = r'''{} {}'''.format(dt.strftime("%b"), dt.strftime("%d")) 168 | # datestr = r'''{} {} {}'''.format(dt.strftime("%b"), dt.strftime("%d"), dt.strftime("%a")) 169 | m = m + r'''\hline 170 | & & \multicolumn{{1}}{{r|}}{{}}\\[-2.0ex] 171 | \textbf{{{}}} & \textbf{{SHA}} & \textbf{{Mer.pass}}\\ 172 | \hline\multicolumn{{3}}{{|r|}}{{}}\\[-2.0ex] 173 | '''.format(datestr) 174 | 175 | p = planetstransit(dfl, True) # True = round to seconds 176 | m = m + r'''Venus & {} & {} \\ 177 | '''.format(p[0],p[1]) 178 | m = m + r'''Mars & {} & {} \\ 179 | '''.format(p[2],p[3]) 180 | m = m + r'''Jupiter & {} & {} \\ 181 | '''.format(p[4],p[5]) 182 | m = m + r'''Saturn & {} & {} \\ 183 | '''.format(p[6],p[7]) 184 | m = m + r'''\hline\multicolumn{3}{c}{}\\ 185 | ''' 186 | out = out + m 187 | 188 | out = out + r'''\end{tabular*} 189 | \par % put next table below here 190 | ''' 191 | return out 192 | 193 | # >>>>>>>>>>>>>>>>>>>>>>>> 194 | def equationtab(date, dpp): 195 | # returns the Equation of Time section for 'date' and 'date+1' 196 | 197 | first_day = r'''{}/{}/{}'''.format(date.year,date.month,date.day) 198 | dfl = ephem.Date(first_day) # convert date to float 199 | 200 | tab = r'''\begin{tabular}[t]{|r|ccc|ccc|} 201 | %\multicolumn{7}{c}{\normalsize{}}\\ 202 | \cline{1-7} 203 | \multicolumn{1}{|c|}{\rule{0pt}{2.4ex}\multirow{4}{*}{\textbf{Day}}} & 204 | \multicolumn{3}{c|}{\textbf{Sun}} & \multicolumn{3}{c|}{\textbf{Moon}}\\ 205 | \multicolumn{1}{|c|}{} & \multicolumn{2}{c}{Eqn.of Time} & \multicolumn{1}{|c|}{Mer.} & \multicolumn{2}{c}{Mer.Pass.} & \multicolumn{1}{|c|}{}\\ 206 | \multicolumn{1}{|c|}{} & \multicolumn{1}{c}{00\textsuperscript{h}} & \multicolumn{1}{c}{12\textsuperscript{h}} & \multicolumn{1}{|c|}{Pass} & \multicolumn{1}{c}{Upper} & \multicolumn{1}{c}{Lower} &\multicolumn{1}{|c|}{Age}\\ 207 | \multicolumn{1}{|c|}{} & \multicolumn{1}{c}{mm:ss} & \multicolumn{1}{c}{mm:ss} & \multicolumn{1}{|c|}{hh:mm:ss} & \multicolumn{1}{c}{hh:mm:ss} & \multicolumn{1}{c}{hh:mm:ss} &\multicolumn{1}{|c|}{}\\ 208 | \cline{1-7}\rule{0pt}{3.0ex}\noindent 209 | ''' 210 | 211 | nn = 0 212 | while nn < dpp: 213 | d = ephem.date(dfl+nn) 214 | eq = equation_of_time(d, True) # True = round to seconds 215 | nn += 1 216 | tab = tab + r'''{} & {} & {} & {} & {} & {} & {}({}\%) \\ 217 | '''.format(d.datetime().strftime("%d"),eq[0],eq[1],eq[2],eq[3],eq[4],eq[5],eq[6]) 218 | 219 | tab = tab + r'''\cline{1-7} 220 | \end{tabular}''' 221 | return tab 222 | 223 | #---------------------- 224 | # page preparation 225 | #---------------------- 226 | 227 | def page(date, dpp=2): 228 | # creates a page (2 days) of tables 229 | 230 | if dpp > 1: 231 | str2 = r'''\textbf{{{} to {}}}'''.format(date.strftime("%Y %B %d"),(date+timedelta(days=dpp-1)).strftime("%b. %d")) 232 | else: 233 | str2 = r'''\textbf{{{}}}'''.format(date.strftime("%Y %B %d")) 234 | 235 | if config.FANCYhd: 236 | page = r''' 237 | % ------------------ N E W P A G E ------------------ 238 | \newpage 239 | \sffamily 240 | \fancyhead[R]{{\textsf{{{}}}}} 241 | \begin{{scriptsize}} 242 | '''.format(str2) 243 | else: # old formatting 244 | page = r''' 245 | % ------------------ N E W P A G E ------------------ 246 | \newpage 247 | \sffamily 248 | \noindent 249 | \begin{{flushright}} 250 | {}% 251 | \end{{flushright}}\par 252 | \begin{{scriptsize}} 253 | '''.format(str2) 254 | 255 | date2 = date + timedelta(days=1) 256 | page += twilighttab(date) 257 | page += meridiantab(date) 258 | if dpp == 2: 259 | page += twilighttab(date2) 260 | page += meridiantab(date2) 261 | page += equationtab(date,dpp) 262 | 263 | # to avoid "Overfull \hbox" messages, leave a paragraph end before the end of a size change. (This may only apply to tabular* table style) See lines below... 264 | page = page + r''' 265 | 266 | \end{scriptsize}''' 267 | return page 268 | 269 | def pages(first_day, dtp): 270 | # dtp = 0 if for entire year; = -1 if for entire month; else days to print 271 | 272 | # make almanac starting from 'date' 273 | out = '' 274 | dpp = 2 # 2 days per page maximum 275 | day1 = first_day 276 | 277 | if dtp == 0: # if entire year 278 | year = first_day.year 279 | yr = year 280 | while year == yr: 281 | day2 = day1 + timedelta(days=1) 282 | if day2.year != yr: 283 | dpp -= day2.day 284 | if dpp <= 0: return out 285 | out += page(day1, dpp) 286 | day1 += timedelta(days=2) 287 | year = day1.year 288 | elif dtp == -1: # if entire month 289 | mth = first_day.month 290 | m = mth 291 | while mth == m: 292 | day2 = day1 + timedelta(days=1) 293 | if day2.month != m: 294 | dpp -= day2.day 295 | if dpp <= 0: return out 296 | out += page(day1, dpp) 297 | day1 += timedelta(days=2) 298 | mth = day1.month 299 | else: # print 'dtp' days beginning with first_day 300 | i = dtp # don't decrement dtp 301 | while i > 0: 302 | if i < 2: dpp = i 303 | out += page(day1, dpp) 304 | i -= 2 305 | day1 += timedelta(days=2) 306 | 307 | return out 308 | 309 | #-------------------------- 310 | # external entry point 311 | #-------------------------- 312 | 313 | def makeEVtables(first_day, dtp): 314 | # make tables starting from first_day 315 | # dtp = 0 if for entire year; = -1 if for entire month; else days to print 316 | 317 | if config.FANCYhd: 318 | return makeEVnew(first_day, dtp) # use the 'fancyhdr' package 319 | else: 320 | return makeEVold(first_day, dtp) # use old formatting 321 | 322 | # The following functions are intentionally separate functions. 323 | # 'makeEVold' is required for TeX Live 2019, which is the standard 324 | # version in Ubuntu 20.04 LTS which expires in April 2030. 325 | 326 | def hdrEVnew(first_day, dtp, vsep1, vsep2): 327 | # build the front page 328 | 329 | tex = r''' 330 | \pagestyle{frontpage} 331 | \begin{titlepage} 332 | \begin{center} 333 | \textsc{\Large Generated using Ephem}\\[0.7cm]''' 334 | 335 | if config.dockerized: # DOCKER ONLY 336 | fn = "../A4chartNorth_P.pdf" 337 | else: 338 | fn = "./A4chartNorth_P.pdf" 339 | 340 | tex += r''' 341 | % TRIM values: left bottom right top 342 | \includegraphics[clip, trim=5mm 8cm 5mm 21mm, width=0.8\textwidth]{{{}}}\\'''.format(fn) 343 | 344 | tex += r'''[{}] 345 | \textsc{{\huge Event Time Tables}}\\[{}]'''.format(vsep1,vsep2) 346 | 347 | if dtp == 0: 348 | tex += r''' 349 | \HRule \\[0.5cm] 350 | {{ \Huge \bfseries {}}}\\[0.2cm] 351 | \HRule \\'''.format(first_day.year) 352 | elif dtp == -1: 353 | tex += r''' 354 | \HRule \\[0.5cm] 355 | {{ \Huge \bfseries {}}}\\[0.2cm] 356 | \HRule \\'''.format(first_day.strftime("%B %Y")) 357 | elif dtp > 1: 358 | tex += r''' 359 | \HRule \\[0.5cm] 360 | {{ \Huge \bfseries {}}}\\[0.2cm] 361 | \HRule \\'''.format(fmtdates(first_day,first_day+timedelta(days=dtp-1))) 362 | else: 363 | tex += r''' 364 | \HRule \\[0.5cm] 365 | {{ \Huge \bfseries {}}}\\[0.2cm] 366 | \HRule \\'''.format(fmtdate(first_day)) 367 | 368 | tex += r''' 369 | \begin{center}\begin{tabular}[t]{rl} 370 | \large\emph{Author:} & \large Andrew \textsc{Bauer}\\ 371 | \end{tabular}\end{center}''' 372 | 373 | tex += r''' 374 | {\large \today} 375 | \HRule \\[0.2cm] 376 | \end{center} 377 | \begin{description}[leftmargin=5.5em,style=nextline]\footnotesize 378 | \item[Disclaimer:] These are computer generated tables. They focus on times of rising and setting events and are rounded to the second (not primarily intended for navigation). Meridian Passage times of the sun, moon and four planets are included. 379 | The author claims no liability for any consequences arising from use of these tables. 380 | \end{description} 381 | \end{titlepage}''' 382 | 383 | return tex 384 | 385 | def makeEVnew(first_day, dtp): 386 | # make tables starting from first_day 387 | # dtp = 0 if for entire year; = -1 if for entire month; else days to print 388 | 389 | # page size specific parameters 390 | # NOTE: 'bm' (bottom margin) is an unrealistic value used only to determine the vertical size of 'body' (textheight), which must be large enough to include all the tables. 'tm' (top margin) and 'hs' (headsep) determine the top of body. Finally use 'fs' (footskip) to position the footer. 391 | 392 | if config.pgsz == "A4": 393 | # A4 ... pay attention to the limited page width 394 | paper = "a4paper" 395 | # title page... 396 | vsep1 = "2.0cm" 397 | vsep2 = "1.5cm" 398 | tm1 = "21mm" 399 | bm1 = "15mm" 400 | lm1 = "10mm" 401 | rm1 = "10mm" 402 | # data pages... 403 | tm = "26.6mm" # was "21mm" [v2q] 404 | bm = "18mm" # was "18mm" [v2q] 405 | hs = "2.6pt" # headsep (page 3 onwards) [v2q] 406 | fs = "15pt" # footskip (page 3 onwards) [v2q] 407 | lm = "16mm" 408 | rm = "16mm" 409 | else: 410 | # LETTER ... pay attention to the limited page height 411 | paper = "letterpaper" 412 | # title page... 413 | vsep1 = "1.5cm" 414 | vsep2 = "1.0cm" 415 | tm1 = "12mm" 416 | bm1 = "15mm" 417 | lm1 = "12mm" 418 | rm1 = "12mm" 419 | # data pages... 420 | tm = "17.8mm" # was "12.2mm" [v2q] 421 | bm = "17mm" # was "13mm" 422 | hs = "2.6pt" # headsep (page 3 onwards) [v2q] 423 | fs = "15pt" # footskip (page 3 onwards) [v2q] 424 | lm = "15mm" 425 | rm = "11mm" 426 | 427 | #------------------------------------------------------------------------------ 428 | # This edition employs the 'fancyhdr' v4 package 429 | # CAUTION: do not use '\newgeometry' & '\restoregeometry' as advised here: 430 | # https://tex.stackexchange.com/questions/247972/top-margin-fancyhdr 431 | #------------------------------------------------------------------------------ 432 | 433 | # default is 'oneside'... 434 | tex = r'''\documentclass[10pt, {}]{{report}}'''.format(paper) 435 | 436 | # document preamble... 437 | tex += r''' 438 | %\usepackage[utf8]{inputenc} 439 | \usepackage[english]{babel} 440 | \usepackage{fontenc} 441 | \usepackage{enumitem} % used to customize the {description} environment''' 442 | 443 | # to troubleshoot add "showframe, verbose," below: 444 | tex += r''' 445 | \usepackage[nomarginpar, top={}, bottom={}, left={}, right={}]{{geometry}}'''.format(tm1,bm1,lm1,rm1) 446 | 447 | # define page styles 448 | # CAUTION: putting '\fancyhf{}' in frontpage style also clears the footer in page2! 449 | tex += r''' 450 | %------------ page styles ------------ 451 | \usepackage{fancyhdr} 452 | \renewcommand{\headrulewidth}{0pt} 453 | \renewcommand{\footrulewidth}{0pt} 454 | \fancypagestyle{frontpage}{ 455 | } 456 | \fancypagestyle{page2}[frontpage]{ 457 | \fancyfootoffset[R]{0pt}% recalculate \headwidth 458 | \cfoot{\centerline{Page \thepage}} 459 | } 460 | \fancypagestyle{datapage}[page2]{''' 461 | tex += r''' 462 | \newgeometry{{nomarginpar, top={}, bottom={}, left={}, right={}, headsep={}, footskip={}}}'''.format(tm,bm,lm,rm,hs,fs) 463 | tex += r''' 464 | \rfoot{\textsf{\footnotesize{https://pypi.org/project/pyalmanac/}}} 465 | } %-----------------------------------''' 466 | 467 | # Note: \DeclareUnicodeCharacter is not compatible with some versions of pdflatex 468 | tex += r''' 469 | \usepackage{xcolor} % highlight double moon events on same day 470 | \definecolor{khaki}{rgb}{0.76, 0.69, 0.57} 471 | \usepackage{multirow} 472 | \newcommand{\HRule}{\rule{\linewidth}{0.5mm}} 473 | \usepackage[pdftex]{graphicx} % for \includegraphics 474 | \usepackage{tikz} % for \draw (load after 'graphicx') 475 | %\showboxbreadth=50 % use for logging 476 | %\showboxdepth=50 % use for logging 477 | %\DeclareUnicodeCharacter{00B0}{\ensuremath{{}^\circ}} 478 | \setlength\fboxsep{1.5pt} % ONLY used by \colorbox in alma_ephem.py 479 | \begin{document}''' 480 | 481 | if not config.DPonly: 482 | tex += hdrEVnew(first_day,dtp,vsep1,vsep2) 483 | 484 | tex += r''' 485 | \pagestyle{datapage} % the default page style for the document''' 486 | 487 | tex += pages(first_day,dtp) 488 | tex += r''' 489 | \end{document}''' 490 | return tex 491 | 492 | # === === === === === === === === === === === === === 493 | # === === === === O L D F O R M A T T I N G === === === === 494 | # === === === === === === === === === === === === === 495 | 496 | def hdrEVold(first_day, dtp, tm1, bm1, lm1, rm1, vsep1, vsep2): 497 | # build the front page 498 | 499 | tex = r''' 500 | % for the title page only... 501 | \newgeometry{{nomarginpar, top={}, bottom={}, left={}, right={}}}'''.format(tm1,bm1,lm1,rm1) 502 | 503 | tex += r''' 504 | \begin{titlepage} 505 | \begin{center} 506 | \textsc{\Large Generated using Ephem}\\[0.7cm]''' 507 | 508 | if config.dockerized: # DOCKER ONLY 509 | fn = "../A4chartNorth_P.pdf" 510 | else: 511 | fn = "./A4chartNorth_P.pdf" 512 | 513 | tex += r''' 514 | % TRIM values: left bottom right top 515 | \includegraphics[clip, trim=5mm 8cm 5mm 21mm, width=0.8\textwidth]{{{}}}\\'''.format(fn) 516 | 517 | tex += r'''[{}] 518 | \textsc{{\huge Event Time Tables}}\\[{}]'''.format(vsep1,vsep2) 519 | 520 | if dtp == 0: 521 | tex += r''' 522 | \HRule \\[0.5cm] 523 | {{ \Huge \bfseries {}}}\\[0.2cm] 524 | \HRule \\'''.format(first_day.year) 525 | elif dtp == -1: 526 | tex += r''' 527 | \HRule \\[0.5cm] 528 | {{ \Huge \bfseries {}}}\\[0.2cm] 529 | \HRule \\'''.format(first_day.strftime("%B %Y")) 530 | elif dtp > 1: 531 | tex += r''' 532 | \HRule \\[0.5cm] 533 | {{ \Huge \bfseries {}}}\\[0.2cm] 534 | \HRule \\'''.format(fmtdates(first_day,first_day+timedelta(days=dtp-1))) 535 | else: 536 | tex += r''' 537 | \HRule \\[0.5cm] 538 | {{ \Huge \bfseries {}}}\\[0.2cm] 539 | \HRule \\'''.format(fmtdate(first_day)) 540 | 541 | tex += r''' 542 | \begin{center}\begin{tabular}[t]{rl} 543 | \large\emph{Author:} & \large Andrew \textsc{Bauer}\\ 544 | \end{tabular}\end{center}''' 545 | 546 | tex += r''' 547 | {\large \today} 548 | \HRule \\[0.2cm] 549 | \end{center} 550 | \begin{description}[leftmargin=5.5em,style=nextline]\footnotesize 551 | \item[Disclaimer:] These are computer generated tables. They focus on times of rising and setting events and are rounded to the second (not primarily intended for navigation). Meridian Passage times of the sun, moon and four planets are included. 552 | The author claims no liability for any consequences arising from use of these tables. 553 | \end{description} 554 | \end{titlepage} 555 | \restoregeometry % so it does not affect the rest of the pages''' 556 | 557 | return tex 558 | 559 | def makeEVold(first_day, dtp): 560 | # make tables starting from first_day 561 | # dtp = 0 if for entire year; = -1 if for entire month; else days to print 562 | 563 | # page size specific parameters 564 | if config.pgsz == "A4": 565 | paper = "a4paper" 566 | vsep1 = "2.0cm" 567 | vsep2 = "1.5cm" 568 | tm1 = "21mm" # title page... 569 | bm1 = "15mm" 570 | lm1 = "10mm" 571 | rm1 = "10mm" 572 | tm = "21mm" # data pages... 573 | bm = "18mm" 574 | lm = "16mm" 575 | rm = "16mm" 576 | else: 577 | paper = "letterpaper" 578 | vsep1 = "1.5cm" 579 | vsep2 = "1.0cm" 580 | tm1 = "12mm" # title page... 581 | bm1 = "15mm" 582 | lm1 = "12mm" 583 | rm1 = "12mm" 584 | tm = "12.2mm" # data pages... 585 | bm = "13mm" 586 | lm = "15mm" 587 | rm = "11mm" 588 | 589 | # default is 'oneside'... 590 | tex = r'''\documentclass[10pt, {}]{{report}}'''.format(paper) 591 | 592 | tex += r''' 593 | %\usepackage[utf8]{inputenc} 594 | \usepackage[english]{babel} 595 | \usepackage{fontenc} 596 | \usepackage{enumitem} % used to customize the {description} environment''' 597 | 598 | # to troubleshoot add "showframe, verbose," below: 599 | tex += r''' 600 | \usepackage[nomarginpar, top={}, bottom={}, left={}, right={}]{{geometry}}'''.format(tm,bm,lm,rm) 601 | 602 | # Note: \DeclareUnicodeCharacter is not compatible with some versions of pdflatex 603 | tex += r''' 604 | \usepackage{xcolor} % highlight double moon events on same day 605 | \definecolor{khaki}{rgb}{0.76, 0.69, 0.57} 606 | \usepackage{multirow} 607 | \newcommand{\HRule}{\rule{\linewidth}{0.5mm}} 608 | \setlength{\footskip}{15pt} 609 | \usepackage[pdftex]{graphicx} % for \includegraphics 610 | \usepackage{tikz} % for \draw (load after 'graphicx') 611 | %\showboxbreadth=50 % use for logging 612 | %\showboxdepth=50 % use for logging 613 | %\DeclareUnicodeCharacter{00B0}{\ensuremath{{}^\circ}} 614 | \setlength\fboxsep{1.5pt} % ONLY used by \colorbox in alma_ephem.py 615 | \begin{document}''' 616 | 617 | if not config.DPonly: 618 | tex += hdrEVold(first_day,dtp,tm1,bm1,lm1,rm1,vsep1,vsep2) 619 | 620 | tex += pages(first_day,dtp) 621 | tex += r''' 622 | \end{document}''' 623 | return tex -------------------------------------------------------------------------------- /increments.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | # Copyright (C) 2014 Enno Rodegerdts 5 | # Copyright (C) 2022 Andrew Bauer 6 | 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | 21 | ###### Standard library imports ###### 22 | import os 23 | import sys 24 | from math import pi, cos, tan, sqrt 25 | 26 | ###### Local application imports ###### 27 | import config 28 | 29 | def degmin(deg): 30 | #changes decimal degrees to the format usually used in the nautical almanac. (ddd°mm.m') 31 | theminus = "" 32 | if deg < 0: 33 | theminus = "-" 34 | deg = abs(deg) 35 | di = int(deg) # degrees (integer) 36 | # note: round() uses "Rounding Half To Even" strategy 37 | mf = round((deg-di)*60, 1) # minutes (float), rounded to 1 decimal place 38 | mi = int(mf) # minutes (integer) 39 | if mi == 60: 40 | mf -= 60 41 | di += 1 42 | if di == 360: 43 | di = 0 44 | gm = "{}{}$^\circ${:04.1f}".format(theminus,di,mf) 45 | return gm 46 | 47 | def decdeg(d,mmm): 48 | # returns decimal degrees from deg. and min. 49 | d=d*1.0 50 | mmm=mmm*1.0 51 | deg=d+(mmm/60) 52 | return deg 53 | 54 | def rad(d,mmm): 55 | #returns radians from deg. and min. 56 | rad=decdeg(d,mmm)/180*pi 57 | return rad 58 | 59 | def suninc(m,s): 60 | # returns the increment for the sun. 61 | mmm=m*1.0 62 | sec=s/60.0 63 | hour=(sec+mmm)/60 64 | inc=degmin(15*hour) 65 | return inc 66 | 67 | def ariesinc(m,s): 68 | # returns the increment for aries 69 | mmm= m*1.0 70 | sec=s/60.0 71 | hour=(sec+mmm)/60 72 | inc=degmin(decdeg(15,2.46)*hour) 73 | return inc 74 | 75 | def mooninc(m,s): 76 | # returns the increment for the Moon 77 | mmm= m*1.0 78 | sec=s/60.0 79 | hour=(sec+mmm)/60 80 | inc=degmin(decdeg(14,19.0)*hour) 81 | return inc 82 | 83 | def vcorr(m,v): 84 | # returns the v correction for a given minute and tabular v. 85 | h=(m+0.5)/60.0 86 | corr=round(v*h,1) 87 | return corr 88 | 89 | def inctab(mmm): 90 | # generates a latex table for increments 91 | 92 | tab = r''' 93 | \noindent 94 | \begin{tabular*}{0.33\textwidth}[t]{@{\extracolsep{\fill}}|>{\bfseries}p{0.3cm}|>{\hspace{-3pt}}r|>{\hspace{-3pt}}r|>{\hspace{-3pt}}r||>{\hspace{-3pt}}c>{\hspace{-3pt}}c>{\hspace{-3pt}}c<{\hspace{-3pt}}|} 95 | \hline 96 | {\tiny m} \textbf{''' 97 | 98 | tab += "{}".format(int(mmm)) 99 | 100 | tab += r'''} & \multicolumn{1}{p{0.5cm}|}{\textbf{Sun Plan.}} & \multicolumn{1}{c|}{\multirow{2}{*}{\textbf{Aries}}} & 101 | \multicolumn{1}{c||}{\multirow{2}{*}{\textbf{Moon}}} & 102 | \multicolumn{3}{c|}{\multirow{2}{*}{\textit{\textbf{v and d corr}}}}\\ 103 | \hline 104 | ''' 105 | 106 | sec = 0 107 | while sec < 60: 108 | line = "{} & {} & {} & {} & {} - {} & {} - {} & {} - {} \\\ \n".format(sec,suninc(mmm,sec),ariesinc(mmm,sec),mooninc(mmm,sec),str(round(0.1*sec,1)),vcorr(mmm,0.1*sec),str(round(6+0.1*sec,1)),vcorr(mmm,6+0.1*sec),str(round(12+0.1*sec,1)),vcorr(mmm,12+0.1*sec)) 109 | tab += line 110 | sec += 1 111 | 112 | tab = tab + r'''\hline \end{tabular*}''' 113 | return tab 114 | 115 | def allinctabs(colsep): 116 | tab = "" 117 | if colsep != "": 118 | # change tabcolsep just for the inctabs 119 | tab += r''' 120 | \newlength{{\oldtabcolsep}} 121 | \setlength{{\oldtabcolsep}}{{\tabcolsep}} 122 | \setlength{{\tabcolsep}}{{{}}}'''.format(colsep) 123 | 124 | # iterates through 60 minutes 125 | mmm=0 126 | while mmm < 60: 127 | tab += inctab(mmm) 128 | mmm += 1 129 | 130 | if colsep != "": 131 | # reset tabcolsep to the previous value (= 6.0pt) 132 | tab += r''' 133 | \setlength{\tabcolsep}{\oldtabcolsep}''' 134 | 135 | return tab 136 | 137 | def dip(meter): 138 | dip=60*0.0293*sqrt(meter) 139 | return dip 140 | 141 | def diptab(): 142 | meter=1 143 | tab = r'''\noindent 144 | \begin{tabular}[t]{|c c c|} 145 | \multicolumn{3}{c}{\textbf{DIP}}\\ 146 | \hline 147 | \textit{m} & \textit{dip} & \textit{ft}\\ 148 | \hline 149 | ''' 150 | while meter < 25.5: 151 | line = "{} & {:.1f} & {:.1f}\\\ \n".format(meter, dip(meter), meter/0.3084) 152 | tab = tab + line 153 | meter += 0.5 154 | tab = tab + r'''\hline 155 | \end{tabular} 156 | ''' 157 | 158 | return tab 159 | 160 | def refrac(h): 161 | r = 1/tan((h+7.31/(h+4.4))/180*pi) 162 | return r 163 | 164 | def refractab(): 165 | ho=5 166 | tab = r''' 167 | \noindent 168 | \begin{tabular}[t]{|c c|} 169 | \multicolumn{2}{c}{\textbf{Refract.}}\\ 170 | \hline 171 | \textit{$H_{a}$} & \textit{ref} \\ 172 | \hline 173 | ''' 174 | while ho < 20: 175 | line = "{}$^\circ$ & {:.1f}\\\ \n".format(ho, refrac(ho)) 176 | tab = tab + line 177 | ho += 0.5 178 | while ho < 40: 179 | line = "{}$^\circ$ & {:.1f}\\\ \n".format(ho, refrac(ho)) 180 | tab = tab + line 181 | ho += 1 182 | while ho < 90: 183 | line = "{}$^\circ$ & {:.1f}\\\ \n".format(ho, refrac(ho)) 184 | tab = tab + line 185 | ho += 5 186 | tab = tab + r'''\hline 187 | \end{tabular} 188 | ''' 189 | return tab 190 | 191 | def parallax(hp, deg, mmm): 192 | #returns parallax in dec minutes from horizontal parallax, and Ha 193 | p = rad(0, hp) * cos(rad(deg, mmm)) * 180/pi *60 194 | return p 195 | 196 | def parallaxtab(): 197 | Hdeg=0 198 | 199 | HP=54.0 200 | tab = r'''\noindent 201 | \begin{tabular}[t]{|c|rrrrrrrrrrrrrrrrrr|} 202 | \multicolumn{19}{c}{\textbf{Parallax of the Moon}}\\ 203 | \hline 204 | ''' 205 | d = 0 206 | line = r"\textbf{$H_{a}$} " 207 | while d<90: 208 | line += r"& \multicolumn{{1}}{{>{{\hspace{{-4pt}}}}c<{{\hspace{{-4pt}}}}|}}{{\textbf{{{}-{}$^\circ$}}}}".format(d, d+5) 209 | d+= 5 210 | line += " \\\ \n \\hline" 211 | tab += line 212 | 213 | while Hdeg < 5 : 214 | #line = " \u0027 " # DOCKER ONLY 215 | line = " $'$ " 216 | dd = Hdeg 217 | while dd < 90: 218 | line += r"& \multicolumn{{1}}{{l}}{{\textbf{{{}$^\circ$}}}}".format(dd) 219 | dd += 5 220 | line += "\\vline \\\ \n" 221 | tab = tab + line 222 | Hmin=0 223 | while Hmin < 60: 224 | dd = Hdeg 225 | line = r"\textbf{{{}}} ".format(Hmin) 226 | while dd < 90: 227 | line += " & {:.1f} ".format(parallax(HP,dd,Hmin)) 228 | dd += 5 229 | line += "\\\ \n" 230 | tab = tab + line 231 | Hmin += 10 232 | Hdeg += 1 233 | 234 | tab += r'''\hline 235 | \multicolumn{1}{|c|}{\textbf{HP}} & \multicolumn{18}{c|}{correction for HP per column}\\ 236 | \hline 237 | ''' 238 | hp = 54.3 239 | while hp<61.5: 240 | line = r"\textbf{{ {:.1f}}} ".format(hp) 241 | d = 2 242 | while d<90: 243 | line += "& {:.1f} ".format(parallax(hp, d, 30) - parallax(54, d, 30)) 244 | d += 5 245 | line += "\\\ \n" 246 | tab += line 247 | hp += 0.3 248 | 249 | 250 | tab = tab + r'''\hline 251 | \end{tabular} 252 | ''' 253 | return tab 254 | 255 | def venparallax(): 256 | Hdeg=10 257 | 258 | tab = r'''\noindent 259 | \begin{tabular}[t]{|c|cccccc|} 260 | \multicolumn{7}{c}{\textbf{Parallax of Venus and Mars}}\\ 261 | ''' 262 | tab += r'''\hline 263 | $H_{a}$ HP & \textbf{.1$'$} & \textbf{.2$'$} & \textbf{.3$'$} & \textbf{.4$'$} & \textbf{.5$'$} & \textbf{.6$'$} \\ 264 | \hline 265 | ''' 266 | while Hdeg<90: 267 | hp = 0.1 268 | line = r"\textbf{{ {}$^\circ$}} ".format(Hdeg) 269 | while hp < 0.7: 270 | line += "& {:.1f} ".format(parallax(hp, Hdeg, 0)) 271 | hp += 0.1 272 | line += "\\\ \n" 273 | tab += line 274 | Hdeg += 10 275 | tab = tab + r'''\hline 276 | \end{tabular} 277 | ''' 278 | return tab 279 | 280 | #-------------------------- 281 | # external entry point 282 | #-------------------------- 283 | 284 | def makelatex(): 285 | 286 | if config.pgsz == "A4": 287 | # A4 ... pay attention to the limited page width 288 | paper = "a4paper" 289 | tm = "15mm" 290 | bm = "15mm" 291 | lm = "8mm" 292 | rm = "8mm" 293 | colsep = "" 294 | else: 295 | # LETTER ... pay attention to the limited page height 296 | paper = "letterpaper" 297 | tm = "15mm" 298 | bm = "15mm" 299 | lm = "8mm" 300 | rm = "8mm" 301 | colsep = "5pt" 302 | 303 | lx = r'''\documentclass[10pt, {}]{{scrreprt}}'''.format(paper) 304 | 305 | lx += r''' 306 | \usepackage[automark]{scrlayer-scrpage} 307 | \pagestyle{scrheadings} 308 | \clearpairofpagestyles 309 | \chead{\large \textbf{Increments and Corrections}} 310 | %\usepackage[utf8]{inputenc} 311 | \usepackage[english]{babel} 312 | \usepackage{fontenc} 313 | %\usepackage{upquote} 314 | \usepackage{multirow} 315 | \usepackage{array, multicol, blindtext}''' 316 | 317 | lx += r''' 318 | \usepackage[landscape,headsep=0mm, headheight=5mm, top={}, bottom={}, left={}, right={}]{{geometry}}'''.format(tm,bm,lm,rm) 319 | 320 | lx += r''' 321 | \newcommand{\HRule}{\rule{\linewidth}{0.9mm}} 322 | \usepackage[pdftex]{graphicx} 323 | %\DeclareUnicodeCharacter{00B0}{\ensuremath{{}^\circ}} 324 | \begin{document} 325 | % ---------------------- 326 | % CAUTION: the next 2 lines suppress Overfull \hbox (badness 10000) messages 327 | \hbadness=10000 328 | \newcount\hbadness 329 | % CAUTION: the next 2 lines suppress Overfull \hbox (too wide) messages below 6.5Pt 330 | \hfuzz=6.5Pt 331 | \newdimen\hfuzz 332 | % ---------------------- 333 | \begin{scriptsize}''' 334 | lx += allinctabs(colsep) 335 | lx += refractab() 336 | lx += parallaxtab() 337 | lx += diptab() 338 | lx += r''' \end{scriptsize} \newpage 339 | \begin{multicols}{2} \begin{scriptsize} 340 | ''' 341 | lx += venparallax() 342 | lx += r'''\end{scriptsize} \newpage 343 | \section*{About these tables} 344 | The preceding static tables are independent from the year. They differ from the tables found in the official paper versions of the Nautical almanac in two important considerations. 345 | \begin{itemize} 346 | \item My tables are not arranged as \textit{critical} tables. So chose the value that fits best to your value and interpolate in the rare cases where this should be necessary. 347 | \item My tables do not combine multiple corrections as some tables in the paper Nautical Almanac do. Each correction has to be applied separately. 348 | \end{itemize} 349 | All tables that are specific for a year are contained in the Nautical Almanac daily pages for the corresponding year. 350 | \subsubsection*{Increments} 351 | The large increment table is is nothing but a linear interpolation between the tabulated values in the daily pages of the Nautical almanac. This table is basically identical with the official one. 352 | \subsubsection*{DIP} 353 | The DIP table corrects for height of eye over the surface. This value has to be subtracted from the sextant altitude ($H_s$). The correction in degrees for height of eye in meters is given by the following formula: 354 | \[d=0.0293\sqrt{m}\] 355 | This is the first correction (apart from index error) that has to be applied to the measured altitude. 356 | \subsubsection*{Refraction} 357 | The next correction is for refraction in the earth's atmosphere. As usual this table is correct for 10$^\circ$C and a pressure of 1010 hPa. This correction has to be applied to apparent altitude ($H_a$). The exact values can be calculated by the following formula. 358 | \[R_0=\cot \left( H_a + \frac{7.31}{H_a+4.4}\right)\] 359 | For other than standard conditions, calculate a correction factor for $R_0$ by: \[f=\frac{0.28P}{T+273}\] where $P$ is the pressure in hectopascal and $T$ is the temperature in $^\circ$C. No table is given for this correction so far. 360 | \subsubsection*{Parallax} 361 | For moon sight (and if necessary for Mars and Venus) a parallax correction is necessary. For Mars and Venus the horizontal parallax ($HP$) is never more than 0.5' and can be omitted if this kind of precision is not necessary. The parallax ($P$) can be calculated from horizontal parallax ($HP$) and apparent altitude $H_a$ with the following formula: 362 | \[P={HP} \times \cos(H_a)\] 363 | The table for the moon gives the parallax for a horizontal parallax of 54' which is the lowest value for the moon. For all other values, the value in the lower half of the table has to be added. Note that this table is only for parallax and does not correct for refraction and semidiameter. For all moon and sun sights, semidiameter has to be added for lower limb sights and subtracted for upper limb sights. The value for HP and semidiameter is tabulated in the daily pages. The smaller parallax table is for parallax of Venus and Mars. 364 | \subsubsection*{Altitude correction} 365 | To correct your sextant altitude $H_s$ do the following: 366 | Calculate $H_a$ by 367 | \[H_a= H_s+I-d\] 368 | Where $I$ is the sextant's index error and $d$ is DIP. Then calculate the observed altitude $H_o$ by 369 | \[H_o= H_a-R+P\pm SD\] 370 | where $R$ is refraction, $P$ is parallax and $SD$ is the semidiameter. 371 | \subsubsection*{Sight reduction} 372 | Sight reduction tables can be downloaded from the US government's internet pages. Search for HO-229 or HO-249. These values can also be calculated with two, relatively simple, formulas: 373 | \[ \sin H_c= \sin L \sin d + \cos L \cos d \cos LHA\] 374 | and 375 | \[\cos A = \frac{\sin d - \sin L \sin H_c}{\cos L \cos H_c}\] 376 | where $A$ is the azimuth angle, $L$ is the latitude, $d$ is the declination and $LHA$ is the local hour angle. The azimuth ($Z_n$) is given by the following rule: 377 | \begin{itemize} 378 | \item if the $LHA$ is greater than $180^\circ$,\quad$Z_n=A$ 379 | \item if the $LHA$ is less than $180^\circ$,\quad$Z_n = 360^\circ - A$ 380 | \end{itemize} 381 | \end{multicols} 382 | \end{document}''' 383 | return lx 384 | 385 | #if sys.version_info[0] != 3: 386 | # raise Exception("This runs with Python 3") 387 | 388 | #fn = "inc" 389 | #filename = fn + ".tex" 390 | #outfile = open(filename, mode="w", encoding="utf8") 391 | #outfile.write(makelatex()) 392 | #outfile.close() 393 | #command = 'pdflatex {}'.format(filename) 394 | #os.system(command) 395 | #print("finished") 396 | #os.remove(fn + ".tex") 397 | #os.remove(fn + ".log") 398 | #os.remove(fn + ".aux") -------------------------------------------------------------------------------- /pyalmanac.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: UTF-8 -*- 3 | 4 | # Copyright (C) 2014 Enno Rodegerdts 5 | # Copyright (C) 2022 Andrew Bauer 6 | 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | 21 | ###### Standard library imports ###### 22 | import os 23 | import sys 24 | import time 25 | from datetime import date, datetime, timedelta 26 | 27 | ###### Local application imports ###### 28 | import config 29 | # !! execute the next 3 lines before importing from nautical/eventtables !! 30 | config.WINpf = True if sys.platform.startswith('win') else False 31 | config.LINUXpf = True if sys.platform.startswith('linux') else False 32 | config.MACOSpf = True if sys.platform == 'darwin' else False 33 | config.FANCYhd = False # default for TeX Live <= "TeX Live 2019/Debian" 34 | import nautical 35 | import suntables 36 | import eventtables 37 | import increments 38 | 39 | 40 | def toUnix(fn): 41 | if config.dockerized or config.LINUXpf or config.MACOSpf: 42 | fn = fn.replace("(","[").replace(")","]") 43 | return fn 44 | 45 | def deletePDF(filename): 46 | if os.path.exists(filename + ".pdf"): 47 | try: 48 | os.remove(filename + ".pdf") 49 | except PermissionError: 50 | print("ERROR: please close '{}' so it can be re-created".format(filename + ".pdf")) 51 | sys.exit(0) 52 | if os.path.exists(filename + ".tex"): 53 | os.remove(filename + ".tex") 54 | 55 | def makePDF(pdfcmd, fn, msg = ""): 56 | command = 'pdflatex {}'.format(pdfcmd + fn + ".tex") 57 | if pdfcmd == "": 58 | os.system(command) 59 | print("finished" + msg) 60 | else: 61 | returned_value = os.system(command) 62 | if returned_value != 0: 63 | if msg != "": 64 | print("ERROR detected while" + msg) 65 | else: 66 | print("!! ERROR detected while creating PDF file !!") 67 | print("!! Append '-v' or '-log' for more information !!") 68 | else: 69 | if msg != "": 70 | print("finished" + msg) 71 | else: 72 | print("finished creating '{}'".format(fn + ".pdf")) 73 | return 74 | 75 | def tidy_up(fn, kl, kt): 76 | if not kt: os.remove(fn + ".tex") 77 | if not kl: 78 | if os.path.isfile(fn + ".log"): 79 | os.remove(fn + ".log") 80 | if os.path.isfile(fn + ".aux"): 81 | os.remove(fn + ".aux") 82 | return 83 | 84 | def check_exists(fn): 85 | # check a required file exist to avoid a more obscure error in pdfTeX if "-v" not used... 86 | if not os.path.exists(fn): 87 | print("Error - missing file: {}".format(fn)) 88 | sys.exit(0) 89 | 90 | def check_mth(mm): 91 | if not 1 <= int(mm) <= 12: 92 | print("ERROR: Enter month between 01 and 12") 93 | sys.exit(0) 94 | 95 | def check_date(year, month, day): 96 | yy = int(year) 97 | mm = int(month) 98 | day_count_for_month = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 99 | if yy%4==0 and (yy%100 != 0 or yy%400==0): 100 | day_count_for_month[2] = 29 101 | if not (1 <= mm <= 12 and 1 <= int(day) <= day_count_for_month[mm]): 102 | print("ERROR: Enter a valid date") 103 | sys.exit(0) 104 | 105 | def check_years(yearfr, yearto): 106 | global yrmin, yrmax 107 | 108 | if str(yearfr).isnumeric(): 109 | if yrmin <= int(yearfr) <= yrmax: 110 | first_day = date(int(yearfr), 1, 1) 111 | else: 112 | print("!! Please pick a year between {} and {} !!".format(yrmin,yrmax)) 113 | sys.exit(0) 114 | else: 115 | print("Error! First year is not numeric") 116 | sys.exit(0) 117 | 118 | if str(yearto).isnumeric(): 119 | if yrmin <= int(yearto) <= yrmax: 120 | first_day_to = date(int(yearto), 1, 1) 121 | else: 122 | print("!! Please pick a year between {} and {} !!".format(yrmin,yrmax)) 123 | sys.exit(0) 124 | if int(yearto) < int(yearfr): 125 | print("Error! The LAST year must be later than the FIRST year") 126 | sys.exit(0) 127 | else: 128 | print("Error! Last year is not numeric") 129 | sys.exit(0) 130 | 131 | 132 | ###### Main Program ###### 133 | 134 | if __name__ == '__main__': 135 | if sys.version_info[0] < 3: 136 | print("This runs only with Python 3") 137 | sys.exit(0) 138 | 139 | # check if TeX Live is compatible with the 'fancyhdr' package... 140 | process = os.popen("tex --version") 141 | returned_value = process.read() 142 | process.close() 143 | if returned_value == "": 144 | print("- - - Neither TeX Live nor MiKTeX is installed - - -") 145 | sys.exit(0) 146 | pos1 = returned_value.find("(") 147 | pos2 = returned_value.find(")") 148 | if pos1 != -1 and pos2 != -1: 149 | texver = returned_value[pos1+1:pos2] 150 | # e.g. "TeX Live 2019/Debian", "TeX Live 2022/dev/Debian", "MiKTeX 22.7.30" 151 | if texver[:8] == "TeX Live": 152 | yrtxt = texver[9:13] 153 | if yrtxt.isnumeric(): 154 | yr = int(yrtxt) 155 | if yr >= 2020: 156 | config.FANCYhd = True # TeX Live can handle the 'fancyhdr' package 157 | # if yr < 2020: 158 | # print("TeX version = '" + texver + "'") 159 | # print("Upgrade TeX Live to 'TeX Live 2020' at least") 160 | # sys.exit(0) 161 | else: 162 | config.FANCYhd = True # assume MiKTeX can handle the 'fancyhdr' package 163 | 164 | # command line arguments... 165 | validargs = ['-v', '-log', '-tex', '-old', 'a4', '-let', '-dpo'] 166 | for i in list(range(1, len(sys.argv))): 167 | if sys.argv[i] not in validargs: 168 | print("Invalid argument: {}".format(sys.argv[i])) 169 | print("\nValid command line arguments are:") 170 | print(" -v ... 'verbose': to send pdfTeX output to the terminal") 171 | print(" -log ... to keep the log file") 172 | print(" -tex ... to keep the tex file") 173 | print(" -old ... old formatting without the fancyhdr package") 174 | print(" -a4 ... A4 papersize") 175 | print(" -let ... Letter papersize") 176 | print(" -dpo ... data pages only") 177 | sys.exit(0) 178 | 179 | # NOTE: pdfTeX 3.14159265-2.6-1.40.21 (TeX Live 2020/Debian), as used in the Docker 180 | # Image, does not have the options "-quiet" or "-verbose". 181 | listarg = "" if "-v" in set(sys.argv[1:]) else "-interaction=batchmode -halt-on-error " 182 | keeplog = True if "-log" in set(sys.argv[1:]) else False 183 | keeptex = True if "-tex" in set(sys.argv[1:]) else False 184 | config.DPonly = True if "-dpo" in set(sys.argv[1:]) else False 185 | if "-old" in set(sys.argv[1:]): config.FANCYhd = False # don't use the 'fancyhdr' package 186 | forcepgsz = False 187 | if not("-a4" in set(sys.argv[1:]) and "-let" in set(sys.argv[1:])): 188 | if "-a4" in set(sys.argv[1:]): forcepgsz = True 189 | if "-let" in set(sys.argv[1:]): forcepgsz = True 190 | if forcepgsz: 191 | if "-a4" in set(sys.argv[1:]): config.pgsz = "A4" 192 | if "-let" in set(sys.argv[1:]): config.pgsz = "Letter" 193 | 194 | d = datetime.utcnow().date() 195 | first_day = date(d.year, d.month, d.day) 196 | yy = "%s" % d.year 197 | 198 | # if this code runs locally (not in Docker), the settings in config.py are used. 199 | # if this code runs in Docker without use of an environment file, the settings in config.py apply. 200 | # if this code runs in Docker with an environment file ("--env-file ./.env"), then its values apply. 201 | if config.dockerized: 202 | docker_main = os.getcwd() 203 | spdf = docker_main + "/" # path to pdf/png/jpg in the Docker Image 204 | config.pgsz = os.getenv('PGSZ', config.pgsz) 205 | config.search_next_rising_sun = os.getenv('SNRS', str(config.search_next_rising_sun)) 206 | stdt = os.getenv('SDATE', 'None') 207 | if stdt != 'None': # for testing a specific date 208 | try: 209 | first_day = date(int(stdt[0:4]), int(stdt[5:7]), int(stdt[8:10])) 210 | except: 211 | print("Invalid date format for SDATE in .env: {}".format(stdt)) 212 | sys.exit(0) 213 | d = first_day 214 | err1 = " the Docker .env file" 215 | err2 = "for SNRS in the Docker .env file" 216 | else: 217 | spad = spdf = "./" # path when executing the GitHub files in a folder 218 | config.search_next_rising_sun = str(config.search_next_rising_sun) 219 | err1 = "config.py" 220 | err2 = "for search_next_rising_sun in config.py" 221 | 222 | if config.pgsz not in set(['A4', 'Letter']): 223 | print("Please choose a valid paper size in {}".format(err1)) 224 | sys.exit(0) 225 | 226 | if config.search_next_rising_sun.lower() not in set(['true', 'false']): 227 | print("Please choose a boolean value {}".format(err2)) 228 | sys.exit(0) 229 | 230 | # ------------ process user input ------------ 231 | 232 | global yrmin, yrmax 233 | yrmin = 1000 234 | yrmax = 3000 235 | config.search_next_rising_sun = (config.search_next_rising_sun.lower() == 'true') # to boolean 236 | f_prefix = config.docker_prefix 237 | f_postfix = config.docker_postfix 238 | 239 | s = input("""\nWhat do you want to create?:\n 240 | 1 Nautical Almanac (for a day/month/year) 241 | 2 Sun tables only (for a day/month/year) 242 | 3 Event Time tables (for a day/month/year) 243 | 4 Nautical almanac - 6 days from today 244 | 5 Sun tables only - 30 days from today 245 | 6 Event Time tables - 6 days from today 246 | 7 "Increments and Corrections" tables (static data) 247 | """) 248 | 249 | if s in set(['1', '2', '3', '4', '5', '6', '7']): 250 | if int(s) < 4: 251 | daystoprocess = 0 252 | ss = input(""" Enter as numeric digits:\n 253 | - starting date as 'DDMMYYYY' 254 | - or just 'YYYY' (for a whole year) 255 | - or 'YYYY-YYYY' (for first and last year) 256 | - or just 'MM' (01 - 12) for the current or a future month 257 | - or '-MM' for a previous month (e.g. '-02' is last February) 258 | - nothing for the current day 259 | """) 260 | sErr = False # syntax error 261 | entireMth = False 262 | entireYr = False 263 | 264 | if len(ss) == 0: 265 | daystoprocess = 1 266 | if d.year > yrmax: 267 | print("!! Only years up to {} are valid!!".format(yrmax)) 268 | sys.exit(0) 269 | else: 270 | if len(ss) not in [2,3,4,8,9]: sErr = True 271 | if len(ss) == 3: 272 | if ss[0] != '-': sErr = True 273 | if not ss[1:].isnumeric(): sErr = True 274 | elif len(ss) == 9: 275 | if ss[4] != '-': sErr = True 276 | if not (ss[:4].isnumeric() and ss[5:].isnumeric()): sErr = True 277 | elif not ss.isnumeric(): sErr = True 278 | 279 | if sErr: 280 | print("ERROR: Enter numeric digits in the correct format") 281 | sys.exit(0) 282 | 283 | if len(ss) == 2: 284 | entireMth = True 285 | dd = "01" 286 | mm = ss[0:2] 287 | check_mth(mm) 288 | if int(mm) < d.month: yy = str(d.year + 1) 289 | elif len(ss) == 3: 290 | entireMth = True 291 | dd = "01" 292 | mm = ss[1:3] 293 | check_mth(mm) 294 | if int(mm) >= d.month: yy = str(d.year - 1) 295 | elif len(ss) == 4: 296 | entireYr = True 297 | dd = "01" 298 | mm = "01" 299 | yy = ss 300 | yearfr = ss 301 | yearto = ss 302 | check_years(yearfr, yearto) 303 | elif len(ss) == 9 and ss[4] == '-': 304 | entireYr = True 305 | dd = "01" 306 | mm = "01" 307 | yy = ss[0:4] 308 | yearfr = ss[0:4] 309 | yearto = ss[5:] 310 | check_years(yearfr, yearto) 311 | elif len(ss) == 8: 312 | dd = ss[:2] 313 | mm = ss[2:4] 314 | check_mth(mm) 315 | yy = ss[4:] 316 | check_date(yy,mm,dd) 317 | 318 | first_day = date(int(yy), int(mm), int(dd)) 319 | d = first_day 320 | 321 | if not entireYr and not entireMth and daystoprocess == 0: 322 | daystoprocess = 1 # default 323 | nn = input(""" Enter number of days to process from starting date: 324 | """) 325 | if len(nn) > 0: 326 | if not nn.isnumeric(): 327 | print("ERROR: Not a number") 328 | sys.exit(0) 329 | daystoprocess = int(nn) 330 | if daystoprocess > 300: 331 | print("ERROR: 'Days to process' not <= 300") 332 | sys.exit(0) 333 | 334 | if s != '3' and int(s) <= 5: 335 | tsin = input(""" What table style is required?:\n 336 | t Traditional 337 | m Modern 338 | """) 339 | ff = '_' 340 | DecFmt = '' 341 | config.tbls = tsin[0:1] # table style 342 | config.decf = tsin[1:2] # Declination format ('+' or nothing) 343 | if config.tbls != 'm': 344 | config.tbls = '' # anything other than 'm' is traditional 345 | ff = '' 346 | if config.decf != '+': # Positive/Negative Declinations 347 | config.decf = '' # USNO format for Declination 348 | else: 349 | DecFmt = '[old]' 350 | 351 | sday = "{:02d}".format(d.day) # sday = "%02d" % d.day 352 | smth = "{:02d}".format(d.month) # smth = "%02d" % d.month 353 | syr = "{}".format(d.year) # syr = "%s" % d.year 354 | symd = syr + smth + sday 355 | sdmy = sday + "." + smth + "." + syr 356 | #print(datetime.now().time()) 357 | papersize = config.pgsz 358 | 359 | # ------------ create the desired tables ------------ 360 | 361 | if s == '1' and entireYr: # Nautical Almanac (for a year) 362 | check_exists(spdf + "A4chartNorth_P.pdf") 363 | print("Please wait - this can take a while.") 364 | for yearint in range(int(yearfr),int(yearto)+1): 365 | start = time.time() 366 | year = "{:4d}".format(yearint) # year = "%4d" %yearint 367 | msg = "\nCreating the nautical almanac for the year {}\n".format(year) 368 | print(msg) 369 | first_day = date(yearint, 1, 1) 370 | ff = "NAtrad" if config.tbls != 'm' else "NAmod" 371 | fn = toUnix("{}({})_{}".format(ff,papersize,year+DecFmt)) 372 | deletePDF(f_prefix + fn) 373 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 374 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 375 | outfile.write(nautical.almanac(first_day,0)) 376 | outfile.close() 377 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 378 | stop = time.time() 379 | msg = "execution time = {:0.2f} seconds".format(stop-start) 380 | print(msg) 381 | print() 382 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 383 | makePDF(listarg, fn) 384 | tidy_up(fn, keeplog, keeptex) 385 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 386 | 387 | if s == '1' and entireMth: # Nautical Almanac (for a month) 388 | check_exists(spdf + "A4chartNorth_P.pdf") 389 | start = time.time() 390 | msg = "\nCreating the nautical almanac for {}".format(first_day.strftime("%B %Y")) 391 | print(msg) 392 | ff = "NAtrad" if config.tbls != 'm' else "NAmod" 393 | fn = toUnix("{}({})_{}".format(ff,papersize,syr + '-' + smth + DecFmt)) 394 | deletePDF(f_prefix + fn) 395 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 396 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 397 | outfile.write(nautical.almanac(first_day,-1)) 398 | outfile.close() 399 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 400 | stop = time.time() 401 | msg = "execution time = {:0.2f} seconds".format(stop-start) 402 | print(msg) 403 | print() 404 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 405 | makePDF(listarg, fn) 406 | tidy_up(fn, keeplog, keeptex) 407 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 408 | 409 | if s == '1' and not entireYr and not entireMth: # Nautical Almanac (for a few days) 410 | check_exists(spdf + "A4chartNorth_P.pdf") 411 | start = time.time() 412 | txt = "from" if daystoprocess > 1 else "for" 413 | msg = "\nCreating the nautical almanac {} {}".format(txt,first_day.strftime("%d %B %Y")) 414 | print(msg) 415 | ff = "NAtrad" if config.tbls != 'm' else "NAmod" 416 | dto = "" 417 | if daystoprocess > 1: # filename as 'from date'-'to date' 418 | lastdate = d + timedelta(days=daystoprocess-1) 419 | dto = lastdate.strftime("-%Y%m%d") 420 | fn = toUnix("{}({})_{}".format(ff,papersize,symd+dto+DecFmt)) 421 | deletePDF(f_prefix + fn) 422 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 423 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 424 | outfile.write(nautical.almanac(first_day,daystoprocess)) 425 | outfile.close() 426 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 427 | stop = time.time() 428 | msg = "execution time = {:0.2f} seconds".format(stop-start) 429 | print(msg) 430 | print() 431 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 432 | makePDF(listarg, fn) 433 | tidy_up(fn, keeplog, keeptex) 434 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 435 | 436 | elif s == '2' and entireYr: # Sun Tables (for a year) 437 | check_exists(spdf + "Ra.jpg") 438 | for yearint in range(int(yearfr),int(yearto)+1): 439 | year = "{:4d}".format(yearint) # year = "%4d" %yearint 440 | msg = "\nCreating the sun tables for the year {}\n".format(year) 441 | print(msg) 442 | first_day = date(yearint, 1, 1) 443 | ff = "STtrad" if config.tbls != 'm' else "STmod" 444 | fn = toUnix("{}({})_{}".format(ff,papersize,year+DecFmt)) 445 | deletePDF(f_prefix + fn) 446 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 447 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 448 | outfile.write(suntables.sunalmanac(first_day,0)) 449 | outfile.close() 450 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 451 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 452 | makePDF(listarg, fn) 453 | tidy_up(fn, keeplog, keeptex) 454 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 455 | 456 | elif s == '2' and entireMth: # Sun Tables (for a month) 457 | check_exists(spdf + "Ra.jpg") 458 | msg = "\nCreating the sun tables for {}".format(first_day.strftime("%B %Y")) 459 | print(msg) 460 | ff = "STtrad" if config.tbls != 'm' else "STmod" 461 | fn = toUnix("{}({})_{}".format(ff,papersize,syr + '-' + smth + DecFmt)) 462 | deletePDF(f_prefix + fn) 463 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 464 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 465 | outfile.write(suntables.sunalmanac(first_day,-1)) 466 | outfile.close() 467 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 468 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 469 | makePDF(listarg, fn) 470 | tidy_up(fn, keeplog, keeptex) 471 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 472 | 473 | elif s == '2' and not entireYr and not entireMth: # Sun Tables (for a few days) 474 | check_exists(spdf + "Ra.jpg") 475 | txt = "from" if daystoprocess > 1 else "for" 476 | msg = "\nCreating the sun tables {} {}".format(txt,first_day.strftime("%d %B %Y")) 477 | print(msg) 478 | ff = "STtrad" if config.tbls != 'm' else "STmod" 479 | dto = "" 480 | if daystoprocess > 1: # filename as 'from date'-'to date' 481 | lastdate = d + timedelta(days=daystoprocess-1) 482 | dto = lastdate.strftime("-%Y%m%d") 483 | fn = toUnix("{}({})_{}".format(ff,papersize,symd+dto+DecFmt)) 484 | deletePDF(f_prefix + fn) 485 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 486 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 487 | outfile.write(suntables.sunalmanac(first_day,daystoprocess)) 488 | outfile.close() 489 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 490 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 491 | makePDF(listarg, fn) 492 | tidy_up(fn, keeplog, keeptex) 493 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 494 | 495 | elif s == '3' and entireYr: # Event Time tables (for a year) 496 | check_exists(spdf + "A4chartNorth_P.pdf") 497 | print("Please wait - this can take a while.") 498 | for yearint in range(int(yearfr),int(yearto)+1): 499 | start = time.time() 500 | year = "{:4d}".format(yearint) # year = "%4d" %yearint 501 | msg = "\nCreating the event time tables for the year {}\n".format(year) 502 | print(msg) 503 | first_day = date(yearint, 1, 1) 504 | fn = toUnix("Event-Times({})_{}".format(papersize,year)) 505 | deletePDF(f_prefix + fn) 506 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 507 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 508 | outfile.write(eventtables.makeEVtables(first_day,0)) 509 | outfile.close() 510 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 511 | stop = time.time() 512 | msg = "execution time = {:0.2f} seconds".format(stop-start) 513 | print(msg) 514 | print() 515 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 516 | makePDF(listarg, fn) 517 | tidy_up(fn, keeplog, keeptex) 518 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 519 | 520 | elif s == '3' and entireMth: # Event Time tables (for a month) 521 | check_exists(spdf + "A4chartNorth_P.pdf") 522 | start = time.time() 523 | msg = "\nCreating the event time tables for {}".format(first_day.strftime("%B %Y")) 524 | print(msg) 525 | fn = toUnix("Event-Times({})_{}".format(papersize,syr + '-' + smth)) 526 | deletePDF(f_prefix + fn) 527 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 528 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 529 | outfile.write(eventtables.makeEVtables(first_day,-1)) 530 | outfile.close() 531 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 532 | stop = time.time() 533 | msg = "execution time = {:0.2f} seconds".format(stop-start) 534 | print(msg) 535 | print() 536 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 537 | makePDF(listarg, fn) 538 | tidy_up(fn, keeplog, keeptex) 539 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 540 | 541 | elif s == '3' and not entireYr and not entireMth: # Event Time tables (for a few days) 542 | check_exists(spdf + "A4chartNorth_P.pdf") 543 | start = time.time() 544 | txt = "from" if daystoprocess > 1 else "for" 545 | msg = "\nCreating the event time tables {} {}".format(txt,first_day.strftime("%d %B %Y")) 546 | print(msg) 547 | fn = toUnix("Event-Times({})_{}".format(papersize,symd)) 548 | if daystoprocess > 1: # filename as 'from date'-'to date' 549 | lastdate = d + timedelta(days=daystoprocess-1) 550 | fn += lastdate.strftime("-%Y%m%d") 551 | deletePDF(f_prefix + fn) 552 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 553 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 554 | outfile.write(eventtables.makeEVtables(first_day,daystoprocess)) 555 | outfile.close() 556 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 557 | stop = time.time() 558 | msg = "execution time = {:0.2f} seconds".format(stop-start) 559 | print(msg) 560 | print() 561 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 562 | makePDF(listarg, fn) 563 | tidy_up(fn, keeplog, keeptex) 564 | if config.dockerized: os.chdir(docker_main) # reset working folder to code folder 565 | 566 | elif s == '4': # Nautical almanac - 6 days from today 567 | check_exists(spdf + "A4chartNorth_P.pdf") 568 | ## config.initLOG() # initialize log file 569 | msg = "\nCreating nautical almanac tables - from {}\n".format(sdmy) 570 | print(msg) 571 | ff = "NAtrad" if config.tbls != 'm' else "NAmod" 572 | lastdate = d + timedelta(days=5) 573 | dto = lastdate.strftime("-%Y%m%d") 574 | fn = toUnix("{}({})_{}".format(ff,papersize,symd+dto+DecFmt)) 575 | deletePDF(f_prefix + fn) 576 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 577 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 578 | outfile.write(nautical.almanac(first_day,6)) 579 | outfile.close() 580 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 581 | ## msg = 'Count of incorrect values: {}'.format(config.errors) 582 | ## config.writeLOG('\n' + msg + '\n') 583 | ## config.closeLOG() 584 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 585 | makePDF(listarg, fn) 586 | tidy_up(fn, keeplog, keeptex) 587 | 588 | elif s == '5': # Sun tables only - 30 days from today 589 | check_exists(spdf + "Ra.jpg") 590 | msg = "\nCreating the sun tables - from {}\n".format(sdmy) 591 | print(msg) 592 | ff = "STtrad" if config.tbls != 'm' else "STmod" 593 | lastdate = d + timedelta(days=29) 594 | dto = lastdate.strftime("-%Y%m%d") 595 | fn = toUnix("{}({})_{}".format(ff,papersize,symd+dto+DecFmt)) 596 | deletePDF(f_prefix + fn) 597 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 598 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 599 | outfile.write(suntables.sunalmanac(first_day,30)) 600 | outfile.close() 601 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 602 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 603 | makePDF(listarg, fn) 604 | tidy_up(fn, keeplog, keeptex) 605 | 606 | elif s == '6': # Event Time tables - 6 days from today 607 | check_exists(spdf + "A4chartNorth_P.pdf") 608 | msg = "\nCreating event time tables - from {}\n".format(sdmy) 609 | print(msg) 610 | fn = toUnix("Event-Times({})_{}".format(papersize,symd)) 611 | lastdate = d + timedelta(days=5) 612 | fn += lastdate.strftime("-%Y%m%d") 613 | deletePDF(f_prefix + fn) 614 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 615 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 616 | outfile.write(eventtables.makeEVtables(first_day,6)) 617 | outfile.close() 618 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 619 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 620 | makePDF(listarg, fn) 621 | tidy_up(fn, keeplog, keeptex) 622 | 623 | elif s == '7': # Increments and Corrections tables 624 | msg = "\nCreating the Increments and Corrections tables\n" 625 | print(msg) 626 | fn = toUnix("Inc({})").format(papersize) 627 | deletePDF(f_prefix + fn) 628 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 629 | outfile = open(f_prefix + fn + ".tex", mode="w", encoding="utf8") 630 | outfile.write(increments.makelatex()) 631 | outfile.close() 632 | # ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: 633 | if config.dockerized: os.chdir(os.getcwd() + f_postfix) # DOCKER ONLY 634 | makePDF(listarg, fn) 635 | tidy_up(fn, keeplog, keeptex) 636 | 637 | else: 638 | print("Error! Choose 1, 2, 3, 4, 5, 6 or 7") 639 | -------------------------------------------------------------------------------- /suntables.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (C) 2014 Enno Rodegerdts 5 | # Copyright (C) 2022 Andrew Bauer 6 | 7 | # This program is free software; you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation; either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License along 18 | # with this program; if not, write to the Free Software Foundation, Inc., 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 20 | 21 | # NOTE: the new format statement requires a literal '{' to be entered as '{{', 22 | # and a literal '}' to be entered as '}}'. The old '%' format specifier 23 | # will be removed from Python at some later time. See: 24 | # https://docs.python.org/3/whatsnew/3.0.html#pep-3101-a-new-approach-to-string-formatting 25 | 26 | ###### Standard library imports ###### 27 | from datetime import datetime, timedelta 28 | from math import copysign, degrees 29 | 30 | ###### Third party imports ###### 31 | import ephem 32 | 33 | ###### Local application imports ###### 34 | from alma_ephem import * 35 | import config 36 | 37 | #---------------------- 38 | # internal methods 39 | #---------------------- 40 | 41 | def fmtdate(d): 42 | if config.pgsz == 'Letter': return d.strftime("%m/%d/%Y") 43 | return d.strftime("%d.%m.%Y") 44 | 45 | def fmtdates(d1,d2): 46 | if config.pgsz == 'Letter': return d1.strftime("%m/%d/%Y") + " - " + d2.strftime("%m/%d/%Y") 47 | return d1.strftime("%d.%m.%Y") + " - " + d2.strftime("%d.%m.%Y") 48 | 49 | def declCompare(prev_rad, curr_rad, next_rad, hr): 50 | # for Declinations only... 51 | # decide if to print N/S; decide if to print degrees 52 | # note: the first three arguments are Ephem angles in radians 53 | prNS = False 54 | prDEG = False 55 | psign = copysign(1.0,prev_rad) 56 | csign = copysign(1.0,curr_rad) 57 | nsign = copysign(1.0,next_rad) 58 | pdeg = abs(degrees(prev_rad)) 59 | cdeg = abs(degrees(curr_rad)) 60 | ndeg = abs(degrees(next_rad)) 61 | pdegi = int(pdeg) 62 | cdegi = int(cdeg) 63 | ndegi = int(ndeg) 64 | pmin = round((pdeg-pdegi)*60, 1) # minutes (float), rounded to 1 decimal place 65 | cmin = round((cdeg-cdegi)*60, 1) # minutes (float), rounded to 1 decimal place 66 | nmin = round((ndeg-ndegi)*60, 1) # minutes (float), rounded to 1 decimal place 67 | pmini = int(pmin) 68 | cmini = int(cmin) 69 | nmini = int(nmin) 70 | if pmini == 60: 71 | pmin -= 60 72 | pdegi += 1 73 | if cmini == 60: 74 | cmin -= 60 75 | cdegi += 1 76 | if nmini == 60: 77 | nmin -= 60 78 | ndegi += 1 79 | # now we have the values in degrees+minutes as printed 80 | 81 | if hr%6 == 0: 82 | prNS = True # print N/S for hour = 0, 6, 12, 18 83 | else: 84 | if psign != csign: 85 | prNS = True # print N/S if previous sign different 86 | if hr < 23: 87 | if csign != nsign: 88 | prNS = True # print N/S if next sign different 89 | if prNS == False: 90 | if pdegi != cdegi: 91 | prDEG = True # print degrees if changed since previous value 92 | if cdegi != ndegi: 93 | prDEG = True # print degrees if next value is changed 94 | else: 95 | prDEG= True # print degrees is N/S to be printed 96 | return prNS, prDEG 97 | 98 | 99 | def NSdecl(deg, hr, printNS, printDEG, modernFMT): 100 | # reformat degrees latitude to Ndd°mm.m or Sdd°mm.m 101 | if deg[0:1] == '-': 102 | hemisph = 'S' 103 | deg = deg[1:] 104 | else: 105 | hemisph = 'N' 106 | if not(printDEG): 107 | deg = deg[10:] # skip the degrees (always dd°mm.m) - note: the degree symbol '$^\circ$' is eight bytes long 108 | if (hr+3)%6 == 0: 109 | deg = r'''\raisebox{0.24ex}{\boldmath$\cdot$~\boldmath$\cdot$~~}''' + deg 110 | if modernFMT: 111 | if printNS or hr%6 == 0: 112 | sdeg = r'''\textcolor{{blue}}{{{}}}'''.format(hemisph) + deg 113 | else: 114 | sdeg = deg 115 | else: 116 | if printNS or hr%6 == 0: 117 | sdeg = r'''\textbf{{{}}}'''.format(hemisph) + deg 118 | else: 119 | sdeg = deg 120 | #print("sdeg: ", sdeg) 121 | return sdeg 122 | 123 | # >>>>>>>>>>>>>>>>>>>>>>>> 124 | def suntab(date, n): 125 | # generates LaTeX table for sun only (traditional) 126 | 127 | first_day = r'''{}/{}/{}'''.format(date.year,date.month,date.day) 128 | dfl = ephem.Date(first_day) # convert date to float 129 | 130 | tab = r'''\noindent 131 | \begin{tabular*}{0.2\textwidth}[t]{@{\extracolsep{\fill}}|c|rr|} 132 | ''' 133 | while n > 0: 134 | dhr = dfl # variable to increment per hour 135 | # print date based on dfl (as Ephem routines use dfl) 136 | tab = tab + r'''\hline 137 | \multicolumn{{1}}{{|c|}}{{\rule{{0pt}}{{2.6ex}}\textbf{{{}}}}} & \multicolumn{{1}}{{c}}{{\textbf{{GHA}}}} & \multicolumn{{1}}{{c|}}{{\textbf{{Dec}}}}\\ 138 | \hline\rule{{0pt}}{{2.6ex}}\noindent 139 | '''.format(ephem.date(dfl).datetime().strftime("%d")) 140 | h = 0 141 | 142 | if config.decf != '+': # USNO format for Declination 143 | # first populate an array of 24 hours with all data 144 | hourlydata = [[] for i in range(24)] 145 | while h < 24: 146 | hourlydata[h] = sunmoon(dhr) 147 | dhr += ephem.hour 148 | h += 1 149 | # now print the data per hour 150 | h = 0 151 | 152 | while h < 24: 153 | eph = hourlydata[h] 154 | if h > 0: 155 | preveph = hourlydata[h-1] 156 | else: 157 | preveph = hourlydata[0] # hour -1 = hour 0 158 | if h < 23: 159 | nexteph = hourlydata[h+1] 160 | else: 161 | nexteph = hourlydata[23] # hour 24 = hour 23 162 | 163 | # format declination checking for hemisphere change 164 | printNS, printDEG = declCompare(preveph[7],eph[7],nexteph[7],h) 165 | sdec = NSdecl(eph[1],h,printNS,printDEG,False) 166 | 167 | line = "{} & {} & {}".format(h,eph[0],sdec) 168 | lineterminator = r'''\\ 169 | ''' 170 | if h < 23 and (h+1)%6 == 0: 171 | lineterminator = r'''\\[2Pt] 172 | ''' 173 | tab += line + lineterminator 174 | h += 1 175 | 176 | else: # Positive/Negative Declinations 177 | while h < 24: 178 | eph = sunmoon(dhr) 179 | line = "{} & {} & {}".format(h,eph[0],eph[1]) 180 | lineterminator = r'''\\ 181 | ''' 182 | if h < 23 and (h+1)%6 == 0: 183 | lineterminator = r'''\\[2Pt] 184 | ''' 185 | tab += line + lineterminator 186 | h += 1 187 | dhr += ephem.hour 188 | 189 | vd = sun_moon_SD(dfl) 190 | tab = tab + r'''\hline 191 | \rule{{0pt}}{{2.4ex}} & 192 | \multicolumn{{1}}{{c}}{{SD={}$'$}} & 193 | \multicolumn{{1}}{{c|}}{{\textit{{d}}\,=\,{}$'$}}\\ 194 | \hline 195 | '''.format(vd[1],vd[0]) 196 | if n > 1: 197 | # add space between tables... 198 | tab = tab + r'''\multicolumn{1}{c}{}\\[-0.5ex]''' 199 | n -= 1 200 | dfl += 1 201 | 202 | tab = tab + r'''\end{tabular*}''' 203 | return tab 204 | 205 | # >>>>>>>>>>>>>>>>>>>>>>>> 206 | def suntabm(date, n): 207 | # generates LaTeX table for sun only (modern) 208 | 209 | first_day = r'''{}/{}/{}'''.format(date.year,date.month,date.day) 210 | dfl = ephem.Date(first_day) # convert date to float 211 | 212 | if config.decf != '+': # USNO format for Declination 213 | colsep = "4pt" 214 | else: 215 | colsep = "3.8pt" 216 | 217 | tab = r'''\noindent 218 | \renewcommand{{\arraystretch}}{{1.1}} 219 | \setlength{{\tabcolsep}}{{{}}} 220 | \begin{{tabular}}[t]{{crr}}'''.format(colsep) 221 | 222 | while n > 0: 223 | dhr = dfl # variable to increment per hour 224 | # print date based on dfl (as Ephem routines use dfl) 225 | tab = tab + r''' 226 | \multicolumn{{1}}{{c}}{{\footnotesize{{\textbf{{{}}}}}}} & \multicolumn{{1}}{{c}}{{\footnotesize{{\textbf{{GHA}}}}}} & \multicolumn{{1}}{{c}}{{\footnotesize{{\textbf{{Dec}}}}}}\\ 227 | \cmidrule{{1-3}} 228 | '''.format(ephem.date(dfl).datetime().strftime("%d")) 229 | h = 0 230 | 231 | if config.decf != '+': # USNO format for Declination 232 | # first populate an array of 24 hours with all data 233 | hourlydata = [[] for i in range(24)] 234 | while h < 24: 235 | hourlydata[h] = sunmoon(dhr) 236 | dhr += ephem.hour 237 | h += 1 238 | # now print the data per hour 239 | h = 0 240 | 241 | while h < 24: 242 | band = int(h/6) 243 | group = band % 2 244 | eph = hourlydata[h] 245 | if h > 0: 246 | preveph = hourlydata[h-1] 247 | else: 248 | preveph = hourlydata[0] # hour -1 = hour 0 249 | if h < 23: 250 | nexteph = hourlydata[h+1] 251 | else: 252 | nexteph = hourlydata[23] # hour 24 = hour 23 253 | 254 | # format declination checking for hemisphere change 255 | printNS, printDEG = declCompare(preveph[7],eph[7],nexteph[7],h) 256 | sdec = NSdecl(eph[1],h,printNS,printDEG,True) 257 | 258 | line = r'''\color{{blue}}{{{}}} & '''.format(h) 259 | line = line + "{} & {}".format(eph[0],sdec) 260 | if group == 1: 261 | tab = tab + r'''\rowcolor{LightCyan}''' 262 | lineterminator = r'''\\ 263 | ''' 264 | if config.pgsz == "A4" and h < 23 and (h+1)%6 == 0: 265 | lineterminator = r'''\\[2Pt] 266 | ''' 267 | tab += line + lineterminator 268 | h += 1 269 | 270 | else: # Positive/Negative Declinations 271 | while h < 24: 272 | band = int(h/6) 273 | group = band % 2 274 | eph = sunmoon(dhr) 275 | line = r'''\color{{blue}}{{{}}} & '''.format(h) 276 | line = line + "{} & {}".format(eph[0],eph[1]) 277 | if group == 1: 278 | tab = tab + r'''\rowcolor{LightCyan}''' 279 | lineterminator = r'''\\ 280 | ''' 281 | if config.pgsz == "A4" and h < 23 and (h+1)%6 == 0: 282 | lineterminator = r'''\\[2Pt] 283 | ''' 284 | tab += line + lineterminator 285 | h += 1 286 | dhr += ephem.hour 287 | 288 | vd = sun_moon_SD(dfl) 289 | tab = tab + r'''\cmidrule{{2-3}} & 290 | \multicolumn{{1}}{{c}}{{\scriptsize{{SD\,=\,{}$'$}}}} & \multicolumn{{1}}{{c}}{{\footnotesize{{\textit{{d}}\,=\,{}$'$}}}}\\ 291 | \cmidrule{{2-3}}'''.format(vd[1],vd[0]) 292 | if n > 1: 293 | # add space between tables... 294 | tab = tab + r''' 295 | \multicolumn{3}{c}{}\\[-1.5ex]''' 296 | n -= 1 297 | dfl += 1 298 | 299 | tab = tab + r''' 300 | \end{tabular}''' 301 | return tab 302 | 303 | #---------------------- 304 | # page preparation 305 | #---------------------- 306 | 307 | def page(date, dpp=15): 308 | 309 | if dpp > 1: 310 | str2 = r'''\textbf{{{} to {}}}'''.format(date.strftime("%Y %B %d"),(date+timedelta(days=dpp-1)).strftime("%b. %d")) 311 | else: 312 | str2 = r'''\textbf{{{}}}'''.format(date.strftime("%Y %B %d")) 313 | 314 | # creates a page(15 days) of the Sun almanac 315 | if config.FANCYhd: 316 | page = r''' 317 | % ------------------ N E W P A G E ------------------ 318 | \newpage 319 | \sffamily 320 | \fancyhead[R]{{\textsf{{{}}}}} 321 | \begin{{scriptsize}} 322 | '''.format(str2) 323 | else: # old formatting 324 | page = r''' 325 | % ------------------ N E W P A G E ------------------ 326 | \newpage 327 | \sffamily 328 | \noindent 329 | \begin{{flushright}} 330 | {}\par 331 | \end{{flushright}} 332 | \begin{{scriptsize}} 333 | '''.format(str2) 334 | 335 | if config.tbls == "m": 336 | while dpp > 0: 337 | page += suntabm(date,min(3,dpp)) 338 | date += timedelta(days=3) 339 | dpp -= 3 340 | if dpp > 0: page = page + r'''\quad 341 | ''' 342 | else: 343 | while dpp > 0: 344 | page += suntab(date,min(3,dpp)) 345 | date += timedelta(days=3) 346 | dpp -= 3 347 | 348 | # to avoid "Overfull \hbox" messages, leave a paragraph end before the end of a size change. (This may only apply to tabular* table style) See lines below... 349 | page = page + r''' 350 | 351 | \end{scriptsize}''' 352 | return page 353 | 354 | 355 | def pages(first_day, dtp): 356 | # dtp = 0 if for entire year; = -1 if for entire month; else days to print 357 | 358 | out = '' 359 | 360 | if dtp == 0: # if entire year 361 | year = first_day.year 362 | yr = year 363 | dpp = 15 # 15 days per page maximum 364 | day1 = first_day 365 | while year == yr: 366 | day15 = day1 + timedelta(days=14) 367 | if day15.year != yr: 368 | dpp -= day15.day 369 | if dpp <= 0: return out 370 | out += page(day1, dpp) 371 | day1 += timedelta(days=15) 372 | year = day1.year 373 | elif dtp == -1: # if entire month 374 | mth = first_day.month 375 | m = mth 376 | dpp = 15 # 15 days per page maximum 377 | day1 = first_day 378 | while mth == m: 379 | day15 = day1 + timedelta(days=14) 380 | if day15.month != m: 381 | dpp -= day15.day 382 | if dpp <= 0: return out 383 | out += page(day1, dpp) 384 | day1 += timedelta(days=15) 385 | mth = day1.month 386 | else: # print 'dtp' days beginning with first_day 387 | day1 = first_day 388 | dpp = 15 # 15 days per page maximum 389 | while dtp > 0: 390 | if dtp <= 15: dpp = dtp 391 | out += page(day1, dpp) 392 | dtp -= 15 393 | day1 += timedelta(days=15) 394 | 395 | return out 396 | 397 | def page2(): 398 | return r''' 399 | \thispagestyle{empty} 400 | \vspace*{2cm} 401 | \noindent 402 | DIP corrects for height of eye over the surface. This value has to be subtracted from the sextant altitude ($H_s$). The correction in degrees for height of eye in meters is given by the following formula: 403 | \[d=0.0293\sqrt{m}\] 404 | This is the first correction (apart from index error) that has to be applied to the measured altitude.\\[12pt] 405 | \noindent 406 | The next correction is for refraction in the earth's atmosphere. As usual this table is correct for 10$^\circ$C and a pressure of 1010 hPa. This correction has to be applied to apparent altitude ($H_a$). The exact values can be calculated by the following formula. 407 | \[R_0=\cot \left( H_a + \frac{7.31}{H_a+4.4}\right)\] 408 | For other than standard conditions, calculate a correction factor for $R_0$ by: \[f=\frac{0.28P}{T+273}\] where $P$ is the pressure in hectopascal and $T$ is the temperature in $^\circ$C.\\[12pt] 409 | \noindent 410 | Semidiameter has to be added for lower limb sights and subtracted for upper limb sights. The value for semidiameter is tabulated in the daily pages.\\[12pt] 411 | \noindent 412 | To correct your sextant altitude $H_s$ do the following: 413 | Calculate $H_a$ by 414 | \[H_a= H_s+I-d\] 415 | where $I$ is the sextant's index error and $d$ is DIP. Then calculate the observed altitude $H_o$ by 416 | \[H_o= H_a-R+P\pm SD\] 417 | where $R$ is refraction, $P$ is parallax and $SD$ is the semidiameter.\\[12pt] 418 | \noindent 419 | Sight reduction tables can be downloaded for the US government's internet pages. Search for HO-229 or HO-249. These values can also be calculated with two, relatively simple, formulas: 420 | \[ \sin H_c= \sin L \sin d + \cos L \cos d \cos LHA\] 421 | and 422 | \[\cos A = \frac{\sin d - \sin L \sin H_c}{\cos L \cos H_c}\] 423 | where $A$ is the azimuth angle, $L$ is the latitude, $d$ is the declination and $LHA$ is the local hour angle. The azimuth ($Z_n$) is given by the following rule: 424 | \begin{itemize} 425 | \item if the $LHA$ is greater than $180^\circ$,\quad$Z_n=A$ 426 | \item if the $LHA$ is less than $180^\circ$,\quad$Z_n = 360^\circ - A$ 427 | \end{itemize}''' 428 | 429 | #-------------------------- 430 | # external entry point 431 | #-------------------------- 432 | 433 | def sunalmanac(first_day, dtp): 434 | # make almanac starting from first_day 435 | 436 | if config.FANCYhd: 437 | return makeSUNnew(first_day, dtp) # use the 'fancyhdr' package 438 | else: 439 | return makeSUNold(first_day, dtp) # use old formatting 440 | 441 | # The following functions are intentionally separate functions. 442 | # 'makeEVold' is required for TeX Live 2019, which is the standard 443 | # version in Ubuntu 20.04 LTS which expires in April 2030. 444 | 445 | def hdrSUNnew(first_day,dtp): 446 | # build the front page 447 | 448 | tex = r''' 449 | \pagestyle{frontpage} 450 | \begin{titlepage} 451 | \vspace*{2cm} 452 | \begin{center} 453 | \textsc{\Large Generated using Ephem}\\[1.5cm]''' 454 | 455 | if config.dockerized: # DOCKER ONLY 456 | fn = "../Ra" 457 | else: 458 | fn = "./Ra" 459 | 460 | tex += r''' 461 | \includegraphics[width=0.4\textwidth]{{{}}}\\[1cm] 462 | \textsc{{\huge The Nautical Almanac for the Sun}}\\[0.7cm]'''.format(fn) 463 | 464 | if dtp == 0: 465 | tex += r''' 466 | \HRule \\[0.6cm] 467 | {{ \Huge \bfseries {}}}\\[0.4cm] 468 | \HRule \\[1.5cm]'''.format(first_day.year) 469 | elif dtp == -1: 470 | tex += r''' 471 | \HRule \\[0.6cm] 472 | {{ \Huge \bfseries {}}}\\[0.4cm] 473 | \HRule \\[1.5cm]'''.format(first_day.strftime("%B %Y")) 474 | elif dtp > 1: 475 | tex += r''' 476 | \HRule \\[0.6cm] 477 | {{ \Huge \bfseries {}}}\\[0.4cm] 478 | \HRule \\[1.5cm]'''.format(fmtdates(first_day,first_day+timedelta(days=dtp-1))) 479 | else: 480 | tex += r''' 481 | \HRule \\[0.6cm] 482 | {{ \Huge \bfseries {}}}\\[0.4cm] 483 | \HRule \\[1.5cm]'''.format(fmtdate(first_day)) 484 | 485 | if config.tbls == "m": 486 | tex += r''' 487 | \begin{center} \large 488 | \emph{Author:}\\ 489 | Andrew \textsc{Bauer}\\[6Pt] 490 | \emph{Original concept from:}\\ 491 | Enno \textsc{Rodegerdts} 492 | \end{center}''' 493 | else: 494 | tex += r''' 495 | \begin{center} \large 496 | \emph{Original author:}\\ 497 | Enno \textsc{Rodegerdts}\\[6Pt] 498 | \emph{Enhancements:}\\ 499 | Andrew \textsc{Bauer} 500 | \end{center}''' 501 | 502 | tex += r''' 503 | \vfill 504 | {\large \today} 505 | \HRule \\[0.6cm] 506 | \end{center} 507 | \begin{description}[leftmargin=5.5em,style=nextline]\footnotesize 508 | \item[Disclaimer:] These are computer generated tables - use them at your own risk. 509 | The accuracy has been randomly checked, but cannot be guaranteed. 510 | The author claims no liability for any consequences arising from use of these tables. 511 | Besides, this publication only contains sun tables: an official version of the Nautical Almanac is indispensable. 512 | \end{description} 513 | \end{titlepage} 514 | \pagestyle{page2}''' 515 | 516 | tex += page2() 517 | 518 | return tex 519 | 520 | def makeSUNnew(first_day, dtp): 521 | # page size specific parameters 522 | # NOTE: 'bm' (bottom margin) is an unrealistic value used only to determine the vertical size of 'body' (textheight), which must be large enough to include all the tables. 'tm' (top margin) and 'hs' (headsep) determine the top of body. Finally use 'fs' (footskip) to position the footer. 523 | 524 | if config.pgsz == "A4": 525 | # A4 ... pay attention to the limited page width 526 | paper = "a4paper" 527 | # title & page 2... 528 | tm1 = "5mm" 529 | bm1 = "13mm" 530 | lm1 = "20mm" 531 | rm1 = "14mm" 532 | # data pages... 533 | tm = "26.6mm" # was "21mm" 534 | bm = "18mm" 535 | hs = "2.3pt" # headsep 536 | fs = "18pt" # footskip 537 | lm = "12mm" # 13mm 538 | rm = "12mm" # 13mm 539 | if config.tbls == "m": # USNO format for Declination 540 | tm = "14mm" # was "8mm" 541 | bm = "13mm" # was "13mm" 542 | hs = "3.4pt" # headsep 543 | fs = "15pt" # footskip 544 | lm = "11mm" 545 | rm = "10mm" 546 | if config.decf == '+': # Positive/Negative Declinations 547 | lm = "12mm" # 14mm 548 | rm = "12mm" # 14mm 549 | else: 550 | # LETTER ... pay attention to the limited page height 551 | paper = "letterpaper" 552 | # title & page 2... 553 | tm1 = "5mm" 554 | bm1 = "13mm" 555 | lm1 = "20mm" 556 | rm1 = "14mm" 557 | # data pages... 558 | tm = "17.8mm" # was "12.2mm" 559 | bm = "18mm" # was "13mm" 560 | hs = "2.6pt" # headsep 561 | fs = "28pt" # footskip 562 | lm = "15mm" # 16mm 563 | rm = "15mm" # 16mm 564 | if config.tbls == "m": # USNO format for Declination 565 | tm = "10.5mm" # was "5mm" 566 | bm = "8mm" # was "8mm" 567 | hs = "1.6pt" # headsep 568 | fs = "14pt" # footskip 569 | lm = "14mm" 570 | rm = "13mm" 571 | if config.decf == '+': # Positive/Negative Declinations 572 | lm = "15mm" 573 | rm = "15mm" 574 | 575 | #------------------------------------------------------------------------------ 576 | # This edition employs the 'fancyhdr' v4.0.3 package 577 | # CAUTION: do not use '\newgeometry' & '\restoregeometry' as advised here: 578 | # https://tex.stackexchange.com/questions/247972/top-margin-fancyhdr 579 | #------------------------------------------------------------------------------ 580 | 581 | # default is 'oneside'... 582 | tex = r'''\documentclass[10pt, {}]{{report}}'''.format(paper) 583 | 584 | # document preamble... 585 | tex += r''' 586 | %\usepackage[utf8]{inputenc} 587 | \usepackage[english]{babel} 588 | \usepackage{fontenc} 589 | \usepackage{enumitem} % used to customize the {description} environment''' 590 | 591 | # to troubleshoot add "showframe, verbose," below: 592 | tex += r''' 593 | \usepackage[nomarginpar, top={}, bottom={}, left={}, right={}]{{geometry}}'''.format(tm1,bm1,lm1,rm1) 594 | 595 | # define page styles 596 | tex += r''' 597 | %------------ page styles ------------ 598 | \usepackage{fancyhdr} 599 | \renewcommand{\headrulewidth}{0pt} 600 | \renewcommand{\footrulewidth}{0pt} 601 | \fancypagestyle{frontpage}{ 602 | % \fancyhf{}% clear all header and footer fields 603 | } 604 | \fancypagestyle{page2}[frontpage]{ 605 | } 606 | \fancypagestyle{datapage}[page2]{''' 607 | tex += r''' 608 | \newgeometry{{nomarginpar, top={}, bottom={}, left={}, right={}, headsep={}, footskip={}}}'''.format(tm,bm,lm,rm,hs,fs) 609 | tex += r''' 610 | \fancyfootoffset[R]{0pt}% recalculate \headwidth 611 | \cfoot{\centerline{Page \thepage}} 612 | \rfoot{\textsf{\footnotesize{https://pypi.org/project/pyalmanac/}}} 613 | } %-----------------------------------''' 614 | 615 | if config.tbls == "m": 616 | tex += r''' 617 | \usepackage[table]{xcolor} 618 | \definecolor{LightCyan}{rgb}{0.88,1,1} 619 | \usepackage{booktabs}''' 620 | 621 | # Note: \DeclareUnicodeCharacter is not compatible with some versions of pdflatex 622 | tex += r''' 623 | \newcommand{\HRule}{\rule{\linewidth}{0.5mm}} 624 | \usepackage[pdftex]{graphicx} 625 | %\showboxbreadth=50 % use for logging 626 | %\showboxdepth=50 % use for logging 627 | %\DeclareUnicodeCharacter{00B0}{\ensuremath{{}^\circ}} 628 | \begin{document}''' 629 | 630 | if not config.DPonly: 631 | tex += hdrSUNnew(first_day,dtp) 632 | 633 | tex += r''' 634 | \pagestyle{datapage} % the default page style for the document 635 | \setcounter{page}{1} % otherwise it's 2''' 636 | 637 | tex += pages(first_day,dtp) 638 | tex += r''' 639 | \end{document}''' 640 | return tex 641 | 642 | # === === === === === === === === === === === === === 643 | # === === === === O L D F O R M A T T I N G === === === === 644 | # === === === === === === === === === === === === === 645 | 646 | def hdrSUNold(first_day, dtp): 647 | # build the front page 648 | 649 | tex = r''' 650 | % for the title page and page 2 only... 651 | \newgeometry{nomarginpar, top=5mm, bottom=13mm, left=20mm, right=14mm} 652 | \begin{titlepage} 653 | \vspace*{2cm} 654 | \begin{center} 655 | \textsc{\Large Generated using Ephem}\\[1.5cm]''' 656 | 657 | if config.dockerized: # DOCKER ONLY 658 | fn = "../Ra" 659 | else: 660 | fn = "./Ra" 661 | 662 | tex += r''' 663 | \includegraphics[width=0.4\textwidth]{{{}}}\\[1cm] 664 | \textsc{{\huge The Nautical Almanac for the Sun}}\\[0.7cm]'''.format(fn) 665 | 666 | if dtp == 0: 667 | tex += r''' 668 | \HRule \\[0.6cm] 669 | {{ \Huge \bfseries {}}}\\[0.4cm] 670 | \HRule \\[1.5cm]'''.format(first_day.year) 671 | elif dtp == -1: 672 | tex += r''' 673 | \HRule \\[0.6cm] 674 | {{ \Huge \bfseries {}}}\\[0.4cm] 675 | \HRule \\[1.5cm]'''.format(first_day.strftime("%B %Y")) 676 | elif dtp > 1: 677 | tex += r''' 678 | \HRule \\[0.6cm] 679 | {{ \Huge \bfseries {}}}\\[0.4cm] 680 | \HRule \\[1.5cm]'''.format(fmtdates(first_day,first_day+timedelta(days=dtp-1))) 681 | else: 682 | tex += r''' 683 | \HRule \\[0.6cm] 684 | {{ \Huge \bfseries {}}}\\[0.4cm] 685 | \HRule \\[1.5cm]'''.format(fmtdate(first_day)) 686 | 687 | if config.tbls == "m": 688 | tex += r''' 689 | \begin{center} \large 690 | \emph{Author:}\\ 691 | Andrew \textsc{Bauer}\\[6Pt] 692 | \emph{Original concept from:}\\ 693 | Enno \textsc{Rodegerdts} 694 | \end{center}''' 695 | else: 696 | tex += r''' 697 | \begin{center} \large 698 | \emph{Original author:}\\ 699 | Enno \textsc{Rodegerdts}\\[6Pt] 700 | \emph{Enhancements:}\\ 701 | Andrew \textsc{Bauer} 702 | \end{center}''' 703 | 704 | tex += r''' 705 | \vfill 706 | {\large \today} 707 | \HRule \\[0.6cm] 708 | \end{center} 709 | \begin{description}[leftmargin=5.5em,style=nextline]\footnotesize 710 | \item[Disclaimer:] These are computer generated tables - use them at your own risk. 711 | The accuracy has been randomly checked, but cannot be guaranteed. 712 | The author claims no liability for any consequences arising from use of these tables. 713 | Besides, this publication only contains sun tables: an official version of the Nautical Almanac is indispensable. 714 | \end{description} 715 | \end{titlepage}''' 716 | 717 | tex += page2() 718 | 719 | tex += r''' 720 | \restoregeometry % so it does not affect the rest of the pages 721 | \setcounter{page}{1} % otherwise it's 2''' 722 | 723 | return tex 724 | 725 | def makeSUNold(first_day, dtp): 726 | # make almanac starting from first_day 727 | # page size specific parameters 728 | 729 | if config.pgsz == "A4": 730 | # pay attention to the limited page width 731 | paper = "a4paper" 732 | tm = "21mm" 733 | bm = "18mm" 734 | lm = "12mm" # 13mm 735 | rm = "12mm" # 13mm 736 | if config.tbls == "m" and config.decf != '+': # USNO format for Declination 737 | tm = "8mm" 738 | bm = "13mm" 739 | lm = "11mm" 740 | rm = "10mm" 741 | if config.tbls == "m" and config.decf == '+': # Positive/Negative Declinations 742 | tm = "8mm" 743 | bm = "13mm" 744 | lm = "12mm" 745 | rm = "12mm" 746 | else: 747 | # pay attention to the limited page height 748 | paper = "letterpaper" 749 | tm = "12.2mm" 750 | bm = "13mm" 751 | lm = "15mm" # 16mm 752 | rm = "15mm" # 16mm 753 | if config.tbls == "m" and config.decf != '+': # USNO format for Declination 754 | tm = "5mm" 755 | bm = "8mm" 756 | lm = "14mm" 757 | rm = "13mm" 758 | if config.tbls == "m" and config.decf == '+': # Positive/Negative Declinations 759 | tm = "5mm" 760 | bm = "8mm" 761 | lm = "15mm" 762 | rm = "15mm" 763 | 764 | # default is 'oneside'... 765 | tex = r'''\documentclass[10pt, {}]{{report}}'''.format(paper) 766 | 767 | tex += r''' 768 | %\usepackage[utf8]{inputenc} 769 | \usepackage[english]{babel} 770 | \usepackage{fontenc} 771 | \usepackage{enumitem} % used to customize the {description} environment''' 772 | 773 | if config.tbls == "m": 774 | tex += r''' 775 | \usepackage[table]{xcolor} 776 | \definecolor{LightCyan}{rgb}{0.88,1,1} 777 | \usepackage{booktabs}''' 778 | 779 | # to troubleshoot add "showframe, verbose," below: 780 | tex += r''' 781 | \usepackage[nomarginpar, top={}, bottom={}, left={}, right={}]{{geometry}}'''.format(tm,bm,lm,rm) 782 | 783 | # Note: \DeclareUnicodeCharacter is not compatible with some versions of pdflatex 784 | tex += r''' 785 | \newcommand{\HRule}{\rule{\linewidth}{0.5mm}} 786 | \setlength{\footskip}{15pt} 787 | \usepackage[pdftex]{graphicx} 788 | %\showboxbreadth=50 % use for logging 789 | %\showboxdepth=50 % use for logging 790 | %\DeclareUnicodeCharacter{00B0}{\ensuremath{{}^\circ}} 791 | \begin{document}''' 792 | 793 | if not config.DPonly: 794 | tex += hdrSUNold(first_day,dtp) 795 | 796 | tex += pages(first_day,dtp) 797 | tex += r''' 798 | \end{document}''' 799 | return tex --------------------------------------------------------------------------------