From 2821e1e301389453258661f9b5d76cd2ade229d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 5 Apr 2026 12:11:47 +0200 Subject: [PATCH] Meteorschauer neu --- README_RECHTESYSTEM.md | 5 + php-server.err.log | 240 ++ public/meteorshowers.php | 4318 ++------------------- public/my_favorites.php | 246 +- public/py/__pycache__/api.cpython-312.pyc | Bin 180375 -> 185442 bytes public/py/api.py | 125 +- 6 files changed, 1013 insertions(+), 3921 deletions(-) create mode 100644 README_RECHTESYSTEM.md diff --git a/README_RECHTESYSTEM.md b/README_RECHTESYSTEM.md new file mode 100644 index 0000000..d90a174 --- /dev/null +++ b/README_RECHTESYSTEM.md @@ -0,0 +1,5 @@ +# Rechtesystem + +Die Anleitung liegt jetzt unter: + +- [docs/README_RECHTESYSTEM.md](/c:/Users/ich/source/repos/skyview.astronomiemuseum.de/docs/README_RECHTESYSTEM.md) diff --git a/php-server.err.log b/php-server.err.log index 21c09ce..5f96d50 100644 --- a/php-server.err.log +++ b/php-server.err.log @@ -799,3 +799,243 @@ Stack trace: [Sun Apr 5 11:12:44 2026] 127.0.0.1:54192 Closing [Sun Apr 5 11:12:45 2026] 127.0.0.1:55784 [200]: GET /favicon.ico [Sun Apr 5 11:12:45 2026] 127.0.0.1:55784 Closing +[Sun Apr 5 11:25:50 2026] 127.0.0.1:63666 Accepted +[Sun Apr 5 11:25:50 2026] 127.0.0.1:63666 [200]: GET /admin_users.php +[Sun Apr 5 11:25:50 2026] 127.0.0.1:63666 Closing +[Sun Apr 5 11:25:50 2026] 127.0.0.1:57197 Accepted +[Sun Apr 5 11:25:50 2026] 127.0.0.1:57197 [200]: GET /css/style.css +[Sun Apr 5 11:25:50 2026] 127.0.0.1:57197 Closing +[Sun Apr 5 11:25:50 2026] 127.0.0.1:52830 Accepted +[Sun Apr 5 11:25:50 2026] 127.0.0.1:52830 [200]: GET /images/star16x16.png +[Sun Apr 5 11:25:50 2026] 127.0.0.1:52830 Closing +[Sun Apr 5 11:25:50 2026] 127.0.0.1:61588 Accepted +[Sun Apr 5 11:25:50 2026] 127.0.0.1:57469 Accepted +[Sun Apr 5 11:25:50 2026] 127.0.0.1:61588 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:25:50 2026] 127.0.0.1:61588 Closing +[Sun Apr 5 11:25:51 2026] 127.0.0.1:57469 [200]: GET /favicon.ico +[Sun Apr 5 11:25:51 2026] 127.0.0.1:57469 Closing +[Sun Apr 5 11:25:57 2026] 127.0.0.1:53473 Accepted +[Sun Apr 5 11:25:57 2026] 127.0.0.1:53473 [302]: POST /admin_users.php +[Sun Apr 5 11:25:57 2026] 127.0.0.1:53473 Closing +[Sun Apr 5 11:25:57 2026] 127.0.0.1:63758 Accepted +[Sun Apr 5 11:25:57 2026] 127.0.0.1:63758 [200]: GET /admin_users.php +[Sun Apr 5 11:25:57 2026] 127.0.0.1:63758 Closing +[Sun Apr 5 11:25:58 2026] 127.0.0.1:55984 Accepted +[Sun Apr 5 11:25:58 2026] 127.0.0.1:55984 [200]: GET /css/style.css +[Sun Apr 5 11:25:58 2026] 127.0.0.1:55984 Closing +[Sun Apr 5 11:25:58 2026] 127.0.0.1:62490 Accepted +[Sun Apr 5 11:25:58 2026] 127.0.0.1:62490 [200]: GET /images/star16x16.png +[Sun Apr 5 11:25:58 2026] 127.0.0.1:62490 Closing +[Sun Apr 5 11:25:58 2026] 127.0.0.1:63631 Accepted +[Sun Apr 5 11:25:58 2026] 127.0.0.1:54175 Accepted +[Sun Apr 5 11:25:58 2026] 127.0.0.1:63631 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:25:58 2026] 127.0.0.1:63631 Closing +[Sun Apr 5 11:25:59 2026] 127.0.0.1:54175 [200]: GET /favicon.ico +[Sun Apr 5 11:25:59 2026] 127.0.0.1:54175 Closing +[Sun Apr 5 11:48:43 2026] 127.0.0.1:52829 Accepted +[Sun Apr 5 11:48:44 2026] 127.0.0.1:52829 [200]: GET /index.php +[Sun Apr 5 11:48:44 2026] 127.0.0.1:52829 Closing +[Sun Apr 5 11:48:44 2026] 127.0.0.1:56008 Accepted +[Sun Apr 5 11:48:44 2026] 127.0.0.1:56008 [200]: GET /css/style.css +[Sun Apr 5 11:48:44 2026] 127.0.0.1:56008 Closing +[Sun Apr 5 11:48:45 2026] 127.0.0.1:60849 Accepted +[Sun Apr 5 11:48:45 2026] 127.0.0.1:58446 Accepted +[Sun Apr 5 11:48:45 2026] 127.0.0.1:60849 [200]: GET /images/star16x16.png +[Sun Apr 5 11:48:45 2026] 127.0.0.1:58446 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:48:45 2026] 127.0.0.1:60849 Closing +[Sun Apr 5 11:48:45 2026] 127.0.0.1:58446 Closing +[Sun Apr 5 11:48:45 2026] 127.0.0.1:51829 Accepted +[Sun Apr 5 11:48:46 2026] 127.0.0.1:51829 [200]: GET /favicon.ico +[Sun Apr 5 11:48:46 2026] 127.0.0.1:51829 Closing +[Sun Apr 5 11:48:46 2026] 127.0.0.1:49634 Accepted +[Sun Apr 5 11:48:59 2026] 127.0.0.1:49634 [200]: GET /meteorshowers.php +[Sun Apr 5 11:48:59 2026] 127.0.0.1:49634 Closing +[Sun Apr 5 11:48:59 2026] 127.0.0.1:59855 Accepted +[Sun Apr 5 11:48:59 2026] 127.0.0.1:59855 [200]: GET /css/style.css +[Sun Apr 5 11:48:59 2026] 127.0.0.1:59855 Closing +[Sun Apr 5 11:48:59 2026] 127.0.0.1:52333 Accepted +[Sun Apr 5 11:48:59 2026] 127.0.0.1:52570 Accepted +[Sun Apr 5 11:48:59 2026] 127.0.0.1:50708 Accepted +[Sun Apr 5 11:48:59 2026] 127.0.0.1:52333 [200]: GET /js/astronomy.browser.min.js +[Sun Apr 5 11:48:59 2026] 127.0.0.1:52570 [200]: GET /images/star16x16.png +[Sun Apr 5 11:48:59 2026] 127.0.0.1:50708 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:48:59 2026] 127.0.0.1:52333 Closing +[Sun Apr 5 11:48:59 2026] 127.0.0.1:52570 Closing +[Sun Apr 5 11:48:59 2026] 127.0.0.1:50708 Closing +[Sun Apr 5 11:49:00 2026] 127.0.0.1:64311 Accepted +[Sun Apr 5 11:49:00 2026] 127.0.0.1:64311 [200]: GET /images/moons/moon4geocron.png +[Sun Apr 5 11:49:00 2026] 127.0.0.1:64311 Closing +[Sun Apr 5 11:49:00 2026] 127.0.0.1:59676 Accepted +[Sun Apr 5 11:49:01 2026] 127.0.0.1:59676 [200]: GET /favicon.ico +[Sun Apr 5 11:49:01 2026] 127.0.0.1:59676 Closing +[Sun Apr 5 11:49:01 2026] 127.0.0.1:64240 Accepted +[Sun Apr 5 11:49:57 2026] 127.0.0.1:61674 Accepted +[Sun Apr 5 11:49:57 2026] 127.0.0.1:61674 [302]: GET /meteorshowers.php +[Sun Apr 5 11:49:57 2026] 127.0.0.1:61674 Closing +[Sun Apr 5 11:49:57 2026] 127.0.0.1:61676 Accepted +[Sun Apr 5 11:49:57 2026] 127.0.0.1:61676 [200]: GET /login.php +[Sun Apr 5 11:49:57 2026] 127.0.0.1:61676 Closing +[Sun Apr 5 11:50:14 2026] 127.0.0.1:64240 Closed without sending a request; it was probably just an unused speculative preconnection +[Sun Apr 5 11:50:14 2026] 127.0.0.1:64240 Closing +[Sun Apr 5 11:50:14 2026] 127.0.0.1:52919 Accepted +[Sun Apr 5 11:50:14 2026] 127.0.0.1:52919 [200]: GET /meteorshowers.php +[Sun Apr 5 11:50:14 2026] 127.0.0.1:52919 Closing +[Sun Apr 5 11:50:14 2026] 127.0.0.1:62156 Accepted +[Sun Apr 5 11:50:14 2026] 127.0.0.1:62156 [200]: GET /css/style.css +[Sun Apr 5 11:50:14 2026] 127.0.0.1:62156 Closing +[Sun Apr 5 11:50:14 2026] 127.0.0.1:52596 Accepted +[Sun Apr 5 11:50:14 2026] 127.0.0.1:52596 [200]: GET /images/star16x16.png +[Sun Apr 5 11:50:14 2026] 127.0.0.1:52596 Closing +[Sun Apr 5 11:50:14 2026] 127.0.0.1:65473 Accepted +[Sun Apr 5 11:50:14 2026] 127.0.0.1:59808 Accepted +[Sun Apr 5 11:50:14 2026] 127.0.0.1:65473 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:50:14 2026] 127.0.0.1:65473 Closing +[Sun Apr 5 11:50:15 2026] 127.0.0.1:59808 [200]: GET /favicon.ico +[Sun Apr 5 11:50:15 2026] 127.0.0.1:59808 Closing +[Sun Apr 5 11:50:32 2026] 127.0.0.1:56279 Accepted +[Sun Apr 5 11:50:32 2026] 127.0.0.1:56279 [200]: GET /meteorshowers.php?q=&mode=active +[Sun Apr 5 11:50:32 2026] 127.0.0.1:56279 Closing +[Sun Apr 5 11:50:32 2026] 127.0.0.1:58009 Accepted +[Sun Apr 5 11:50:32 2026] 127.0.0.1:58009 [200]: GET /css/style.css +[Sun Apr 5 11:50:32 2026] 127.0.0.1:58009 Closing +[Sun Apr 5 11:50:32 2026] 127.0.0.1:50221 Accepted +[Sun Apr 5 11:50:32 2026] 127.0.0.1:50221 [200]: GET /images/star16x16.png +[Sun Apr 5 11:50:32 2026] 127.0.0.1:50221 Closing +[Sun Apr 5 11:50:32 2026] 127.0.0.1:64906 Accepted +[Sun Apr 5 11:50:32 2026] 127.0.0.1:51891 Accepted +[Sun Apr 5 11:50:32 2026] 127.0.0.1:64906 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:50:32 2026] 127.0.0.1:64906 Closing +[Sun Apr 5 11:50:33 2026] 127.0.0.1:51891 [200]: GET /favicon.ico +[Sun Apr 5 11:50:33 2026] 127.0.0.1:51891 Closing +[Sun Apr 5 11:50:35 2026] 127.0.0.1:55431 Accepted +[Sun Apr 5 11:50:35 2026] 127.0.0.1:55431 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 11:50:35 2026] 127.0.0.1:55431 Closing +[Sun Apr 5 11:50:35 2026] 127.0.0.1:56566 Accepted +[Sun Apr 5 11:50:35 2026] 127.0.0.1:56566 [200]: GET /css/style.css +[Sun Apr 5 11:50:35 2026] 127.0.0.1:56566 Closing +[Sun Apr 5 11:50:35 2026] 127.0.0.1:61013 Accepted +[Sun Apr 5 11:50:35 2026] 127.0.0.1:61013 [200]: GET /images/star16x16.png +[Sun Apr 5 11:50:35 2026] 127.0.0.1:61013 Closing +[Sun Apr 5 11:50:35 2026] 127.0.0.1:58944 Accepted +[Sun Apr 5 11:50:35 2026] 127.0.0.1:56599 Accepted +[Sun Apr 5 11:50:35 2026] 127.0.0.1:58944 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:50:35 2026] 127.0.0.1:58944 Closing +[Sun Apr 5 11:50:36 2026] 127.0.0.1:56599 [200]: GET /favicon.ico +[Sun Apr 5 11:50:36 2026] 127.0.0.1:56599 Closing +[Sun Apr 5 11:54:38 2026] 127.0.0.1:57364 Accepted +[Sun Apr 5 11:54:38 2026] 127.0.0.1:57364 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 11:54:38 2026] 127.0.0.1:57364 Closing +[Sun Apr 5 11:54:38 2026] 127.0.0.1:50807 Accepted +[Sun Apr 5 11:54:38 2026] 127.0.0.1:50807 [200]: GET /css/style.css +[Sun Apr 5 11:54:38 2026] 127.0.0.1:50807 Closing +[Sun Apr 5 11:54:38 2026] 127.0.0.1:59570 Accepted +[Sun Apr 5 11:54:38 2026] 127.0.0.1:59570 [200]: GET /images/star16x16.png +[Sun Apr 5 11:54:38 2026] 127.0.0.1:59570 Closing +[Sun Apr 5 11:54:39 2026] 127.0.0.1:57873 Accepted +[Sun Apr 5 11:54:39 2026] 127.0.0.1:54508 Accepted +[Sun Apr 5 11:54:39 2026] 127.0.0.1:57873 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:54:39 2026] 127.0.0.1:57873 Closing +[Sun Apr 5 11:54:39 2026] 127.0.0.1:54508 [200]: GET /favicon.ico +[Sun Apr 5 11:54:39 2026] 127.0.0.1:54508 Closing +[Sun Apr 5 11:55:26 2026] 127.0.0.1:63857 Accepted +[Sun Apr 5 11:55:26 2026] 127.0.0.1:63857 [302]: GET /meteorshowers.php +[Sun Apr 5 11:55:26 2026] 127.0.0.1:63857 Closing +[Sun Apr 5 11:55:26 2026] 127.0.0.1:63859 Accepted +[Sun Apr 5 11:55:26 2026] 127.0.0.1:63859 [200]: GET /login.php +[Sun Apr 5 11:55:26 2026] 127.0.0.1:63859 Closing +[Sun Apr 5 11:56:30 2026] 127.0.0.1:51672 Accepted +[Sun Apr 5 11:56:30 2026] 127.0.0.1:51672 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 11:56:30 2026] 127.0.0.1:51672 Closing +[Sun Apr 5 11:56:30 2026] 127.0.0.1:57680 Accepted +[Sun Apr 5 11:56:30 2026] 127.0.0.1:57680 [200]: GET /css/style.css +[Sun Apr 5 11:56:30 2026] 127.0.0.1:57680 Closing +[Sun Apr 5 11:56:31 2026] 127.0.0.1:57516 Accepted +[Sun Apr 5 11:56:31 2026] 127.0.0.1:57516 [200]: GET /images/star16x16.png +[Sun Apr 5 11:56:31 2026] 127.0.0.1:57516 Closing +[Sun Apr 5 11:56:31 2026] 127.0.0.1:64583 Accepted +[Sun Apr 5 11:56:31 2026] 127.0.0.1:55172 Accepted +[Sun Apr 5 11:56:31 2026] 127.0.0.1:64583 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:56:31 2026] 127.0.0.1:64583 Closing +[Sun Apr 5 11:56:32 2026] 127.0.0.1:55172 [200]: GET /favicon.ico +[Sun Apr 5 11:56:32 2026] 127.0.0.1:55172 Closing +[Sun Apr 5 11:56:36 2026] 127.0.0.1:51181 Accepted +[Sun Apr 5 11:56:36 2026] 127.0.0.1:51181 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 11:56:36 2026] 127.0.0.1:51181 Closing +[Sun Apr 5 11:56:36 2026] 127.0.0.1:63170 Accepted +[Sun Apr 5 11:56:36 2026] 127.0.0.1:63170 [200]: GET /css/style.css +[Sun Apr 5 11:56:36 2026] 127.0.0.1:63170 Closing +[Sun Apr 5 11:56:36 2026] 127.0.0.1:58615 Accepted +[Sun Apr 5 11:56:36 2026] 127.0.0.1:58615 [200]: GET /images/star16x16.png +[Sun Apr 5 11:56:36 2026] 127.0.0.1:58615 Closing +[Sun Apr 5 11:56:36 2026] 127.0.0.1:59905 Accepted +[Sun Apr 5 11:56:36 2026] 127.0.0.1:49289 Accepted +[Sun Apr 5 11:56:36 2026] 127.0.0.1:59905 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:56:36 2026] 127.0.0.1:59905 Closing +[Sun Apr 5 11:56:37 2026] 127.0.0.1:49289 [200]: GET /favicon.ico +[Sun Apr 5 11:56:37 2026] 127.0.0.1:49289 Closing +[Sun Apr 5 11:57:43 2026] 127.0.0.1:51973 Accepted +[Sun Apr 5 11:57:43 2026] 127.0.0.1:51973 [302]: GET /meteorshowers.php +[Sun Apr 5 11:57:43 2026] 127.0.0.1:51973 Closing +[Sun Apr 5 11:57:43 2026] 127.0.0.1:51975 Accepted +[Sun Apr 5 11:57:43 2026] 127.0.0.1:51975 [200]: GET /login.php +[Sun Apr 5 11:57:43 2026] 127.0.0.1:51975 Closing +[Sun Apr 5 11:58:09 2026] 127.0.0.1:62492 Accepted +[Sun Apr 5 11:58:09 2026] 127.0.0.1:62492 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 11:58:09 2026] 127.0.0.1:62492 Closing +[Sun Apr 5 11:58:09 2026] 127.0.0.1:52376 Accepted +[Sun Apr 5 11:58:09 2026] 127.0.0.1:52376 [200]: GET /css/style.css +[Sun Apr 5 11:58:09 2026] 127.0.0.1:52376 Closing +[Sun Apr 5 11:58:09 2026] 127.0.0.1:50060 Accepted +[Sun Apr 5 11:58:09 2026] 127.0.0.1:50060 [200]: GET /images/star16x16.png +[Sun Apr 5 11:58:09 2026] 127.0.0.1:50060 Closing +[Sun Apr 5 11:58:09 2026] 127.0.0.1:63671 Accepted +[Sun Apr 5 11:58:09 2026] 127.0.0.1:63196 Accepted +[Sun Apr 5 11:58:09 2026] 127.0.0.1:63671 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:58:09 2026] 127.0.0.1:63671 Closing +[Sun Apr 5 11:58:10 2026] 127.0.0.1:63196 [200]: GET /favicon.ico +[Sun Apr 5 11:58:10 2026] 127.0.0.1:63196 Closing +[Sun Apr 5 11:59:27 2026] 127.0.0.1:52816 Accepted +[Sun Apr 5 11:59:27 2026] 127.0.0.1:52816 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 11:59:27 2026] 127.0.0.1:52816 Closing +[Sun Apr 5 11:59:27 2026] 127.0.0.1:54341 Accepted +[Sun Apr 5 11:59:27 2026] 127.0.0.1:54341 [200]: GET /css/style.css +[Sun Apr 5 11:59:27 2026] 127.0.0.1:54341 Closing +[Sun Apr 5 11:59:27 2026] 127.0.0.1:49273 Accepted +[Sun Apr 5 11:59:27 2026] 127.0.0.1:49273 [200]: GET /images/star16x16.png +[Sun Apr 5 11:59:27 2026] 127.0.0.1:49273 Closing +[Sun Apr 5 11:59:27 2026] 127.0.0.1:55327 Accepted +[Sun Apr 5 11:59:27 2026] 127.0.0.1:56745 Accepted +[Sun Apr 5 11:59:27 2026] 127.0.0.1:55327 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 11:59:27 2026] 127.0.0.1:55327 Closing +[Sun Apr 5 11:59:28 2026] 127.0.0.1:56745 [200]: GET /favicon.ico +[Sun Apr 5 11:59:28 2026] 127.0.0.1:56745 Closing +[Sun Apr 5 12:02:37 2026] 127.0.0.1:63271 Accepted +[Sun Apr 5 12:02:37 2026] 127.0.0.1:63271 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 12:02:37 2026] 127.0.0.1:63271 Closing +[Sun Apr 5 12:02:37 2026] 127.0.0.1:60905 Accepted +[Sun Apr 5 12:02:37 2026] 127.0.0.1:60905 [200]: GET /css/style.css +[Sun Apr 5 12:02:37 2026] 127.0.0.1:60905 Closing +[Sun Apr 5 12:02:38 2026] 127.0.0.1:61012 Accepted +[Sun Apr 5 12:02:38 2026] 127.0.0.1:61012 [200]: GET /images/star16x16.png +[Sun Apr 5 12:02:38 2026] 127.0.0.1:61012 Closing +[Sun Apr 5 12:02:38 2026] 127.0.0.1:60220 Accepted +[Sun Apr 5 12:02:38 2026] 127.0.0.1:49322 Accepted +[Sun Apr 5 12:02:38 2026] 127.0.0.1:60220 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 12:02:38 2026] 127.0.0.1:60220 Closing +[Sun Apr 5 12:02:39 2026] 127.0.0.1:49322 [200]: GET /favicon.ico +[Sun Apr 5 12:02:39 2026] 127.0.0.1:49322 Closing +[Sun Apr 5 12:02:49 2026] 127.0.0.1:55826 Accepted +[Sun Apr 5 12:02:49 2026] 127.0.0.1:55826 [200]: GET /meteorshowers.php?q=&mode=all +[Sun Apr 5 12:02:49 2026] 127.0.0.1:55826 Closing +[Sun Apr 5 12:02:49 2026] 127.0.0.1:58635 Accepted +[Sun Apr 5 12:02:49 2026] 127.0.0.1:58635 [200]: GET /css/style.css +[Sun Apr 5 12:02:49 2026] 127.0.0.1:58635 Closing +[Sun Apr 5 12:02:50 2026] 127.0.0.1:55997 Accepted +[Sun Apr 5 12:02:50 2026] 127.0.0.1:55997 [200]: GET /images/star16x16.png +[Sun Apr 5 12:02:50 2026] 127.0.0.1:55997 Closing +[Sun Apr 5 12:02:50 2026] 127.0.0.1:55677 Accepted +[Sun Apr 5 12:02:50 2026] 127.0.0.1:62995 Accepted +[Sun Apr 5 12:02:50 2026] 127.0.0.1:55677 [200]: GET /images/star16x16_rays.png +[Sun Apr 5 12:02:50 2026] 127.0.0.1:55677 Closing +[Sun Apr 5 12:02:51 2026] 127.0.0.1:62995 [200]: GET /favicon.ico +[Sun Apr 5 12:02:51 2026] 127.0.0.1:62995 Closing diff --git a/public/meteorshowers.php b/public/meteorshowers.php index efc44e4..a035272 100644 --- a/public/meteorshowers.php +++ b/public/meteorshowers.php @@ -4,3949 +4,479 @@ declare(strict_types=1); session_start(); $loggedIn = isset($_SESSION['user_id']); -$pageTitle = 'Monatsvorhersage - AstroTools'; -$bodyClass = 'admin-page monatsvorhersage-page'; +$pageTitle = 'Meteorstroeme - AstroTools'; +$bodyClass = 'admin-page meteorshowers-page'; $publicBasePath = './'; - -$currentYear = (int) date('Y'); -$selectedYear = isset($_GET['year']) ? (int) $_GET['year'] : $currentYear; -$selectedMonth = isset($_GET['month']) ? max(1, min(12, (int) $_GET['month'])) : (int) date('n'); -// ------------------------------------------------------------------------- -// Grenzwerte fuer Sternkarte und Objektanzeige -// ------------------------------------------------------------------------- -// Maximale Helligkeit (Magnitude) der Sterne, die aus der Datenbank geladen -// werden. Hoehere Werte laden mehr, aber auch schwaechere Sterne. -$starDatabaseMagnitudeLimit = 6.0; - -// Maximale Helligkeit der Sterne, die auf der Monats-Sternkarte sichtbar -// gezeichnet werden sollen. -$starDisplayMagnitudeLimit = 4.4; - -// Sterne bis zu dieser Helligkeit erhalten auf der Sternkarte einen Namen. -$starLabelMagnitudeLimit = 2.0; - -// Maximale Helligkeit der Messier-Objekte, die aus der Datenbank geladen -// werden. -$messierDatabaseMagnitudeLimit = 2.0; - -// Messier-Objekte bis zu dieser Helligkeit werden auf der Monatskarte -// eingeblendet. -$messierDisplayMagnitudeLimit = 1.0; - -// ------------------------------------------------------------------------- -// Grenzwerte fuer Monatsereignisse -// ------------------------------------------------------------------------- -// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und Planeten. -$moonPlanetMaxSeparationDeg = 1.5; - -// Maximaler Winkelabstand fuer Begegnungen zwischen Mond und ausgewaehlten -// Sternhaufen wie Plejaden oder Praesepe. -$moonDeepSkyMaxSeparationDeg = 1.0; - -// Maximaler Winkelabstand fuer Begegnungen zwischen hellen Planeten und -// ausgewaehlten hellen Sternen wie Regulus oder Spica. -$planetBrightStarMaxSeparationDeg = 1.0; - -// Maximaler Winkelabstand fuer Planet-Planet-Konjunktionen. -$planetConjunctionMaxSeparationDeg = 1.0; - -// Nur Sterne bis zu dieser Helligkeit werden fuer Mondbedeckungen -// beruecksichtigt. -$moonOccultationMaxMag = 2.0; - -// ------------------------------------------------------------------------- -// Schwellen fuer "Gute Merkur-Sichtbarkeit" -// ------------------------------------------------------------------------- -// Gute Merkur-Sichtbarkeit wird nicht nur ueber Elongation definiert, sondern -// ueber echte Beobachtbarkeit in der Daemmerung. -// Geprueft wird pro Tag: -// morgens eine feste Zahl Minuten VOR Sonnenaufgang, -// abends eine feste Zahl Minuten NACH Sonnenuntergang. -$mercuryGoodVisibilitySampleOffsetMinutes = 45; - -// Merkur muss zum Pruefzeitpunkt mindestens diese Hoehe ueber dem Horizont -// erreichen, damit der Tag als gut sichtbar gilt. -$mercuryGoodVisibilityMinAltitudeDeg = 7.0; - -// Gleichzeitig muss die Sonne ausreichend tief unter dem Horizont stehen. -// So vermeiden wir Treffer, bei denen Merkur zwar formal sichtbar waere, aber -// noch zu stark in der hellen Daemmerung untergeht. -$mercuryGoodVisibilityMaxSunAltitudeDeg = -3.0; - -// Erst ab dieser Anzahl aufeinanderfolgender guter Tage erzeugen wir ein -// Monatsereignis. Einzelne Ausreisser-Tage werden dadurch herausgefiltert. -$mercuryGoodVisibilityMinConsecutiveDays = 3; - -// ------------------------------------------------------------------------- -// Schwellen fuer "Planetenparade" -// ------------------------------------------------------------------------- -// Geprueft wird morgens eine feste Zahl Minuten VOR Sonnenaufgang und abends -// eine feste Zahl Minuten NACH Sonnenuntergang. -$planetParadeSampleOffsetMinutes = 45; - -// Ein Planet zaehlt fuer die Parade nur dann mit, wenn er zum Pruefzeitpunkt -// mindestens diese Hoehe ueber dem Horizont erreicht. -$planetParadeMinAltitudeDeg = 7.0; - -// Gleichzeitig muss die Sonne bereits tief genug unter dem Horizont stehen, -// damit die Parade nicht nur rechnerisch, sondern auch praktisch sichtbar ist. -$planetParadeMaxSunAltitudeDeg = -4.0; - -// Die beteiligten Planeten muessen innerhalb dieses Azimutbogens liegen. -// So vermeiden wir Treffer, bei denen die Planeten zu weit ueber den Himmel -// verteilt sind und nicht mehr als zusammenhaengende Parade wirken. -$planetParadeMaxAzimuthSpanDeg = 120.0; - -// Mindestens so viele helle Planeten muessen gleichzeitig sichtbar sein, -// damit wir von einer Planetenparade sprechen. -$planetParadeMinPlanetCount = 4; - -// Erst ab dieser Anzahl aufeinanderfolgender Tage erzeugen wir ein Ereignis. -// Das filtert kurzlebige Ein-Tages-Treffer heraus. -$planetParadeMinConsecutiveDays = 2; - -$years = range($currentYear - 2, $currentYear + 5); -$months = [ - 1 => 'Januar', - 2 => 'Februar', - 3 => 'Maerz', - 4 => 'April', - 5 => 'Mai', - 6 => 'Juni', - 7 => 'Juli', - 8 => 'August', - 9 => 'September', - 10 => 'Oktober', - 11 => 'November', - 12 => 'Dezember', -]; -$monthSkyTimes = [ - 1 => ['hour' => 22, 'minute' => 0], - 2 => ['hour' => 22, 'minute' => 0], - 3 => ['hour' => 22, 'minute' => 0], - 4 => ['hour' => 23, 'minute' => 0], - 5 => ['hour' => 23, 'minute' => 0], - 6 => ['hour' => 23, 'minute' => 30], - 7 => ['hour' => 23, 'minute' => 30], - 8 => ['hour' => 23, 'minute' => 0], - 9 => ['hour' => 23, 'minute' => 0], - 10 => ['hour' => 23, 'minute' => 0], - 11 => ['hour' => 22, 'minute' => 0], - 12 => ['hour' => 22, 'minute' => 0], -]; - -$starData = []; -$lineStarData = []; -$constellationLines = []; -$messierData = []; -$planetData = []; -$moonData = null; -$moonPhaseEvents = []; -$goldenHandleEvents = []; -$moonPlanetApproaches = []; -$moonDeepSkyApproaches = []; -$planetBrightStarApproaches = []; -$mercuryGoodVisibilityEvents = []; -$planetParadeEvents = []; -$planetConstellationChangeEvents = []; -$planetConjunctions = []; -$eclipseEvents = []; -$moonApsisEvents = []; -$sunApsisEvents = []; -$innerPlanetElongationEvents = []; -$venusPeakMagnitudeEvents = []; -$outerPlanetEvents = []; -$outerPlanetStationEvents = []; -$jupiterMoonSideEvents = []; -$moonOccultations = []; -$meteorShowerPeaks = []; -$seasonChanges = []; -$timeChanges = []; -$monthEventList = []; -$starDataError = null; - -if (!function_exists('buildMonthOccultationStarLabel')) { - function buildMonthOccultationStarLabel(array $row): string - { - $proper = trim((string) ($row['proper'] ?? '')); - if ($proper !== '') { - return $proper; - } - - $bayer = trim((string) ($row['bayer'] ?? '')); - $con = trim((string) ($row['con'] ?? '')); - if ($bayer !== '') { - return $con !== '' ? $bayer . ' ' . $con : $bayer; - } - - $hip = isset($row['hip']) ? (int) $row['hip'] : 0; - return $hip > 0 ? 'HIP ' . $hip : 'Unbenannter Stern'; - } -} - -if (!function_exists('normalizeMonthForecastDegree')) { - function normalizeMonthForecastDegree(float $value): float - { - $normalized = fmod($value, 360.0); - if ($normalized < 0.0) { - $normalized += 360.0; - } - - return $normalized; - } -} - -if (!function_exists('monthForecastSolarLongitude')) { - function monthForecastSolarLongitude(DateTimeImmutable $dateTime): float - { - $julianDay = ((float) $dateTime->format('U')) / 86400.0 + 2440587.5; - $daysSinceJ2000 = $julianDay - 2451545.0; - $meanLongitude = normalizeMonthForecastDegree(280.46 + 0.9856474 * $daysSinceJ2000); - $meanAnomalyRad = deg2rad(normalizeMonthForecastDegree(357.528 + 0.9856003 * $daysSinceJ2000)); - - return normalizeMonthForecastDegree( - $meanLongitude - + 1.915 * sin($meanAnomalyRad) - + 0.020 * sin(2.0 * $meanAnomalyRad) - ); - } -} - -if (!function_exists('monthForecastForwardDegreeDistance')) { - function monthForecastForwardDegreeDistance(float $from, float $to): float - { - return normalizeMonthForecastDegree($to - $from); - } -} - -if (!function_exists('monthForecastRadiantIsVisible')) { - function monthForecastRadiantIsVisible(float $latitudeDeg, ?float $declinationDeg): bool - { - if ($declinationDeg === null) { - return false; - } - - return abs($latitudeDeg - $declinationDeg) < 90.0; - } -} - -if (!function_exists('findMonthForecastSolarLongitudeCrossing')) { - function findMonthForecastSolarLongitudeCrossing( - DateTimeImmutable $rangeStartLocal, - DateTimeImmutable $rangeEndLocal, - float $targetLongitude, - DateTimeZone $displayTimezone - ): ?DateTimeImmutable { - $utc = new DateTimeZone('UTC'); - $rangeStartUtc = $rangeStartLocal->setTimezone($utc); - $rangeEndUtc = $rangeEndLocal->setTimezone($utc); - - $startLongitude = monthForecastSolarLongitude($rangeStartUtc); - $endLongitude = monthForecastSolarLongitude($rangeEndUtc); - $targetDelta = monthForecastForwardDegreeDistance($startLongitude, $targetLongitude); - $rangeDelta = monthForecastForwardDegreeDistance($startLongitude, $endLongitude); - - if ($targetDelta > $rangeDelta + 0.0001) { - return null; - } - - $low = (int) $rangeStartUtc->format('U'); - $high = (int) $rangeEndUtc->format('U'); - - for ($i = 0; $i < 40; $i++) { - $mid = (int) floor(($low + $high) / 2); - $midLongitude = monthForecastSolarLongitude(new DateTimeImmutable('@' . $mid)); - $midDelta = monthForecastForwardDegreeDistance($startLongitude, $midLongitude); - - if ($midDelta < $targetDelta) { - $low = $mid; - } else { - $high = $mid; - } - } - - return (new DateTimeImmutable('@' . $high))->setTimezone($displayTimezone); - } -} - -if (!function_exists('buildObservationTipInsertSql')) { - function buildObservationTipInsertSql(array $events, DateTimeZone $timezone): string - { - $rows = []; - - foreach ($events as $event) { - if (!is_array($event)) { - continue; - } - - $tip = trim((string) ($event['event'] ?? '')); - if ($tip === '') { - continue; - } - - $moment = null; - $localIso = trim((string) ($event['local_iso'] ?? '')); - if ($localIso !== '') { - try { - $moment = new DateTimeImmutable($localIso); - } catch (Throwable $e) { - $moment = null; - } - } - - if (!$moment instanceof DateTimeImmutable) { - $date = trim((string) ($event['date'] ?? '')); - $time = trim((string) ($event['time'] ?? '')); - if ($date !== '' && $time !== '') { - $moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null; - } - } - - if (!$moment instanceof DateTimeImmutable) { - continue; - } - - $rows[] = sprintf( - "('%s', '%s')", - $moment->setTimezone($timezone)->format('Y-m-d H:i:s'), - str_replace("'", "''", $tip) - ); - } - - if ($rows === []) { - return ''; - } - - return "INSERT INTO `astro_sonobs_observationtips` (`moment`, `tip`)\nVALUES\n " - . implode(",\n ", $rows) - . ";\n"; - } -} - -if (!function_exists('escapeIcalText')) { - function escapeIcalText(string $value): string - { - return str_replace( - ["\\", ";", ",", "\r\n", "\n", "\r"], - ["\\\\", "\\;", "\\,", "\\n", "\\n", "\\n"], - $value - ); - } -} - -if (!function_exists('foldIcalLine')) { - function foldIcalLine(string $line): string - { - $chunks = preg_split('/(.{1,73})/u', $line, -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY); - if ($chunks === false || $chunks === []) { - return $line; - } - - $firstChunk = array_shift($chunks); - return $firstChunk . ($chunks ? "\r\n " . implode("\r\n ", $chunks) : ''); - } -} - -if (!function_exists('buildObservationTipCalendarIcs')) { - function buildObservationTipCalendarIcs(array $events, DateTimeZone $timezone): string - { - $lines = [ - 'BEGIN:VCALENDAR', - 'VERSION:2.0', - 'PRODID:-//AstroTools//Monatsvorhersage//DE', - 'CALSCALE:GREGORIAN', - 'METHOD:PUBLISH', - 'X-WR-CALNAME:AstroTools Monatsereignisse', - ]; - - $createdUtc = (new DateTimeImmutable('now', new DateTimeZone('UTC')))->format('Ymd\THis\Z'); - - foreach ($events as $index => $event) { - if (!is_array($event)) { - continue; - } - - $summary = trim((string) ($event['event'] ?? '')); - if ($summary === '') { - continue; - } - - $moment = null; - $localIso = trim((string) ($event['local_iso'] ?? '')); - if ($localIso !== '') { - try { - $moment = new DateTimeImmutable($localIso); - } catch (Throwable $e) { - $moment = null; - } - } - - if (!$moment instanceof DateTimeImmutable) { - $date = trim((string) ($event['date'] ?? '')); - $time = trim((string) ($event['time'] ?? '')); - if ($date !== '' && $time !== '') { - $moment = DateTimeImmutable::createFromFormat('d.m.Y H:i', $date . ' ' . $time, $timezone) ?: null; - } - } - - if (!$moment instanceof DateTimeImmutable) { - continue; - } - - $startUtc = $moment->setTimezone(new DateTimeZone('UTC')); - $endMoment = null; - $endLocalIso = trim((string) ($event['end_local_iso'] ?? '')); - if ($endLocalIso !== '') { - try { - $endMoment = new DateTimeImmutable($endLocalIso); - } catch (Throwable $e) { - $endMoment = null; - } - } - - if (!$endMoment instanceof DateTimeImmutable) { - $durationMinutes = isset($event['duration_minutes']) ? max(1, (int) $event['duration_minutes']) : 15; - $endMoment = $moment->modify('+' . $durationMinutes . ' minutes'); - } - - $endUtc = $endMoment->setTimezone(new DateTimeZone('UTC')); - $uidBase = sha1($startUtc->format(DateTimeInterface::ATOM) . '|' . $summary . '|' . (string) $index); - - $eventLines = [ - 'BEGIN:VEVENT', - 'UID:' . $uidBase . '@astrotools.local', - 'DTSTAMP:' . $createdUtc, - 'DTSTART:' . $startUtc->format('Ymd\THis\Z'), - 'DTEND:' . $endUtc->format('Ymd\THis\Z'), - 'SUMMARY:' . escapeIcalText($summary), - 'DESCRIPTION:' . escapeIcalText($summary), - 'END:VEVENT', - ]; - - foreach ($eventLines as $eventLine) { - $lines[] = foldIcalLine($eventLine); - } - } - - $lines[] = 'END:VCALENDAR'; - - return implode("\r\n", $lines) . "\r\n"; - } -} - -try { - $dbConfig = require __DIR__ . '/../config/database.php'; - $dsn = sprintf( - 'mysql:host=%s;dbname=%s;charset=%s', - $dbConfig['host'], - $dbConfig['dbname'], - $dbConfig['charset'] ?? 'utf8mb4' - ); - $pdo = new PDO($dsn, $dbConfig['user'], $dbConfig['pass'], [ +$headerIntroPre = 'Beobachtung'; +$headerIntroTitle = 'Meteorstroeme'; +$headerIntroSub = 'Liste der importierten Meteorstroeme aus der Datenbank mit Angaben in solarer Laenge.'; + +$config = require __DIR__ . '/../config/database.php'; + +$dsn = sprintf( + 'mysql:host=%s;dbname=%s;charset=%s', + $config['host'], + $config['dbname'], + $config['charset'] ?? 'utf8mb4' +); + +$pdo = new PDO( + $dsn, + $config['user'], + $config['pass'], + [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, - ]); + ] +); - $starStmt = $pdo->query( - "SELECT hip, proper, bayer, con, spect, mag, ci, rarad, decrad - FROM star_hipparcos - WHERE mag IS NOT NULL - AND rarad IS NOT NULL - AND decrad IS NOT NULL - AND mag <= " . (float) $starDatabaseMagnitudeLimit . " - ORDER BY mag ASC" - ); +$searchQuery = trim((string) ($_GET['q'] ?? '')); +$showMode = trim((string) ($_GET['mode'] ?? 'all')); +$allowedModes = ['all', 'active']; +if (!in_array($showMode, $allowedModes, true)) { + $showMode = 'all'; +} - while ($row = $starStmt->fetch()) { - $starData[] = [ - 'hip' => $row['hip'] !== null ? (int) $row['hip'] : null, - 'proper' => trim((string) ($row['proper'] ?? '')), - 'bayer' => trim((string) ($row['bayer'] ?? '')), - 'con' => trim((string) ($row['con'] ?? '')), - 'spect' => trim((string) ($row['spect'] ?? '')), - 'ra' => (float) $row['rarad'], - 'dec' => (float) $row['decrad'], - 'mag' => (float) $row['mag'], - 'ci' => $row['ci'] !== null ? (float) $row['ci'] : null, - ]; +function h(?string $value): string +{ + return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8'); +} + +function normalizeSolarLongitude(float $value): float +{ + $normalized = fmod($value, 360.0); + if ($normalized < 0.0) { + $normalized += 360.0; } - $constellationIndex = []; - $ensureConstellation = static function (string $id) use (&$constellationIndex): void { - if (!isset($constellationIndex[$id])) { - $constellationIndex[$id] = [ - 'id' => $id, - 'lines' => [], - 'label_positions' => [], - 'common_name' => [ - 'german' => null, - 'native' => null, - 'byname' => null, - ], + return $normalized; +} + +function currentSolarLongitude(DateTimeImmutable $dateTime): float +{ + $julianDay = ((float) $dateTime->format('U')) / 86400.0 + 2440587.5; + $daysSinceJ2000 = $julianDay - 2451545.0; + $meanLongitude = normalizeSolarLongitude(280.46 + 0.9856474 * $daysSinceJ2000); + $meanAnomalyRad = deg2rad(normalizeSolarLongitude(357.528 + 0.9856003 * $daysSinceJ2000)); + + return normalizeSolarLongitude( + $meanLongitude + + 1.915 * sin($meanAnomalyRad) + + 0.020 * sin(2.0 * $meanAnomalyRad) + ); +} + +function fetchCurrentSolarLongitude(DateTimeZone $displayTimezone): ?float +{ + $pythonResult = runPythonApi('current_solar_longitude', [$displayTimezone->getName()]); + if (!$pythonResult['ok']) { + return null; + } + + $solarLongitude = $pythonResult['data']['result']['solar_longitude_deg'] ?? null; + if (!is_numeric($solarLongitude)) { + return null; + } + + return normalizeSolarLongitude((float) $solarLongitude); +} + +function runPythonApi(string $action, array $args): array +{ + $scriptPath = __DIR__ . '/py/api.py'; + $pythonCandidates = PHP_OS_FAMILY === 'Windows' + ? ['python', 'py'] + : ['/usr/bin/python3', '/usr/bin/python', 'python3', 'python']; + + $escapedArguments = array_map('escapeshellarg', array_merge([$scriptPath, $action], $args)); + $lastError = 'Python konnte nicht gestartet werden.'; + + foreach ($pythonCandidates as $pythonBinary) { + $command = $pythonBinary . ' ' . implode(' ', $escapedArguments) . ' 2>&1'; + $output = []; + $resultCode = 0; + exec($command, $output, $resultCode); + + $joined = trim(implode("\n", $output)); + $decoded = json_decode($joined, true); + + if (is_array($decoded)) { + return [ + 'ok' => (bool) ($decoded['ok'] ?? false), + 'data' => $decoded, + 'raw' => $joined, + 'command' => $command, ]; } - }; - $nameStmt = $pdo->query( - "SELECT constellation_id, german_name, native_name, byname - FROM constellation_names - ORDER BY constellation_id" - ); - foreach ($nameStmt as $row) { - $constellationId = (string) $row['constellation_id']; - $ensureConstellation($constellationId); - $constellationIndex[$constellationId]['common_name'] = [ - 'german' => $row['german_name'] !== null ? (string) $row['german_name'] : null, - 'native' => $row['native_name'] !== null ? (string) $row['native_name'] : null, - 'byname' => $row['byname'] !== null ? (string) $row['byname'] : null, - ]; - } - - $labelStmt = $pdo->query( - "SELECT constellation_id, position_index, ra_hours, dec_degrees - FROM constellation_label_positions - ORDER BY constellation_id, position_index" - ); - foreach ($labelStmt as $row) { - $constellationId = (string) $row['constellation_id']; - $ensureConstellation($constellationId); - $constellationIndex[$constellationId]['label_positions'][] = [ - (float) $row['ra_hours'], - (float) $row['dec_degrees'], - ]; - } - - $lineStmt = $pdo->query( - "SELECT cl.constellation_id, cl.line_index, clp.point_index, clp.hip_number - FROM constellation_lines cl - INNER JOIN constellation_line_points clp ON clp.line_id = cl.id - ORDER BY cl.constellation_id, cl.line_index, clp.point_index" - ); - - foreach ($lineStmt as $row) { - $constellationId = (string) $row['constellation_id']; - $lineIndex = (int) $row['line_index']; - - $ensureConstellation($constellationId); - - $constellationIndex[$constellationId]['lines'][$lineIndex] ??= []; - $constellationIndex[$constellationId]['lines'][$lineIndex][] = (int) $row['hip_number']; - } - - foreach ($constellationIndex as &$constellation) { - if ($constellation['lines']) { - ksort($constellation['lines']); - $constellation['lines'] = array_values($constellation['lines']); + if ($joined !== '') { + $lastError = $joined; + } elseif ($resultCode !== 0) { + $lastError = 'Fehlercode ' . $resultCode . ' bei ' . $pythonBinary; } } - unset($constellation); - $constellationLines = array_values($constellationIndex); + return [ + 'ok' => false, + 'error' => $lastError, + ]; +} - $lineStarStmt = $pdo->query( - "SELECT DISTINCT sh.hip, sh.proper, sh.bayer, sh.con, sh.spect, sh.mag, sh.ci, sh.rarad, sh.decrad - FROM constellation_line_points clp - INNER JOIN star_hipparcos sh ON sh.hip = clp.hip_number - WHERE sh.rarad IS NOT NULL - AND sh.decrad IS NOT NULL" - ); - - while ($row = $lineStarStmt->fetch()) { - $lineStarData[] = [ - 'hip' => $row['hip'] !== null ? (int) $row['hip'] : null, - 'proper' => trim((string) ($row['proper'] ?? '')), - 'bayer' => trim((string) ($row['bayer'] ?? '')), - 'con' => trim((string) ($row['con'] ?? '')), - 'spect' => trim((string) ($row['spect'] ?? '')), - 'ra' => (float) $row['rarad'], - 'dec' => (float) $row['decrad'], - 'mag' => $row['mag'] !== null ? (float) $row['mag'] : 99.0, - 'ci' => $row['ci'] !== null ? (float) $row['ci'] : null, - ]; +function isSolarLongitudeInRange(float $current, ?float $start, ?float $finish): bool +{ + if ($start === null || $finish === null) { + return false; } - $messierStmt = $pdo->query( - "SELECT messier, name, object_type_abrev, ra_deg, dec_deg, v_mag, major_axis - FROM dso_objects - WHERE messier IS NOT NULL - AND messier <> '' - AND ra_deg IS NOT NULL - AND dec_deg IS NOT NULL - AND v_mag IS NOT NULL - AND v_mag <= " . (float) $messierDatabaseMagnitudeLimit . " - ORDER BY v_mag ASC, CAST(REGEXP_REPLACE(messier, '[^0-9]', '') AS UNSIGNED)" - ); + $current = normalizeSolarLongitude($current); + $start = normalizeSolarLongitude($start); + $finish = normalizeSolarLongitude($finish); - while ($row = $messierStmt->fetch()) { - if (!preg_match('/(\d+)/', (string) $row['messier'], $match)) { - continue; + if ($start <= $finish) { + return $current >= $start && $current <= $finish; + } + + return $current >= $start || $current <= $finish; +} + +function solarLongitudeToUtcDate(?float $targetLongitude, int $year, DateTimeZone $displayTimezone): ?DateTimeImmutable +{ + if ($targetLongitude === null) { + return null; + } + + static $cache = []; + + $cacheKey = $year . '|' . $displayTimezone->getName() . '|' . number_format((float) $targetLongitude, 6, '.', ''); + if (array_key_exists($cacheKey, $cache)) { + return $cache[$cacheKey]; + } + + $pythonResult = runPythonApi('solar_longitude_to_datetime', [ + (string) $targetLongitude, + (string) $year, + $displayTimezone->getName(), + ]); + + if (!$pythonResult['ok']) { + $cache[$cacheKey] = null; + return null; + } + + $resultData = $pythonResult['data']; + $utcIso = trim((string) ($resultData['result']['utc_iso'] ?? '')); + if ($utcIso === '') { + $cache[$cacheKey] = null; + return null; + } + + try { + $cache[$cacheKey] = new DateTimeImmutable($utcIso); + } catch (Throwable $e) { + $cache[$cacheKey] = null; + return null; + } + + return $cache[$cacheKey]; +} + +function warmSolarLongitudeDateCache(array $showers, int $year, DateTimeZone $displayTimezone): void +{ + $uniqueLongitudes = []; + + foreach ($showers as $shower) { + foreach (['active_start_sol', 'active_finish_sol', 'peak_sol'] as $field) { + if (!isset($shower[$field]) || $shower[$field] === null || $shower[$field] === '') { + continue; + } + + $key = number_format((float) $shower[$field], 6, '.', ''); + $uniqueLongitudes[$key] = (float) $shower[$field]; } - - $messierData[] = [ - 'm' => (int) $match[1], - 'name' => trim((string) ($row['name'] ?? '')), - 'type' => trim((string) ($row['object_type_abrev'] ?? 'As')), - 'ra' => ((float) $row['ra_deg']) / 15.0, - 'dec' => (float) $row['dec_deg'], - 'mag' => (float) $row['v_mag'], - 'size' => $row['major_axis'] !== null ? (float) $row['major_axis'] : 8.0, - ]; } - $meteorStmt = $pdo->query( - "SELECT designation, shower_code, radiant_delta_deg, peak_sol, zhr, zhr_variable - FROM meteorshowers - WHERE peak_sol IS NOT NULL - ORDER BY peak_sol ASC, designation ASC" + foreach ($uniqueLongitudes as $solarLongitude) { + solarLongitudeToUtcDate($solarLongitude, $year, $displayTimezone); + } +} + +function formatNullableNumber(mixed $value, int $decimals = 1, string $suffix = ''): string +{ + if ($value === null || $value === '') { + return '-'; + } + + return number_format((float) $value, $decimals, ',', '.') . $suffix; +} + +function formatSolarLongitudeWithDate(?float $solarLongitude, int $year, DateTimeZone $displayTimezone): string +{ + if ($solarLongitude === null) { + return '-'; + } + + $label = number_format((float) $solarLongitude, 2, ',', '.') . '°'; + $date = solarLongitudeToUtcDate($solarLongitude, $year, $displayTimezone); + + if (!$date instanceof DateTimeImmutable) { + return $label; + } + + return $label . '
' . htmlspecialchars($date->setTimezone($displayTimezone)->format('d.m.Y'), ENT_QUOTES, 'UTF-8') . ''; +} + +$displayTimezone = new DateTimeZone(date_default_timezone_get() ?: 'Europe/Berlin'); +$displayYear = (int) (new DateTimeImmutable('now', $displayTimezone))->format('Y'); +$todaySolarLongitude = fetchCurrentSolarLongitude($displayTimezone) + ?? currentSolarLongitude(new DateTimeImmutable('now', new DateTimeZone('UTC'))); + +$sql = " + SELECT + `id`, + `shower_code`, + `iau_no`, + `designation`, + `speed_kms`, + `radiant_alpha_deg`, + `radiant_delta_deg`, + `parent_object`, + `population_index`, + `active_start_sol`, + `active_finish_sol`, + `peak_sol`, + `zhr`, + `zhr_variable`, + `has_yearly_variants` + FROM `meteorshowers` + WHERE 1 = 1 +"; + +$params = []; + +if ($searchQuery !== '') { + $sql .= " + AND ( + `shower_code` LIKE :query + OR `designation` LIKE :query + OR `parent_object` LIKE :query + ) + "; + $params[':query'] = '%' . $searchQuery . '%'; +} + +$sql .= " + ORDER BY + COALESCE(`zhr`, 0) DESC, + `peak_sol` IS NULL ASC, + `peak_sol` ASC, + `designation` ASC +"; + +$stmt = $pdo->prepare($sql); +$stmt->execute($params); +$allShowers = $stmt->fetchAll(); + +$showers = []; +$activeCount = 0; + +foreach ($allShowers as $shower) { + $isActive = isSolarLongitudeInRange( + $todaySolarLongitude, + isset($shower['active_start_sol']) ? (float) $shower['active_start_sol'] : null, + isset($shower['active_finish_sol']) ? (float) $shower['active_finish_sol'] : null ); - while ($row = $meteorStmt->fetch()) { - $meteorShowerPeaks[] = [ - 'designation' => trim((string) ($row['designation'] ?? '')), - 'shower_code' => trim((string) ($row['shower_code'] ?? '')), - 'radiant_delta_deg' => $row['radiant_delta_deg'] !== null ? (float) $row['radiant_delta_deg'] : null, - 'peak_sol' => (float) $row['peak_sol'], - 'zhr' => $row['zhr'] !== null ? (int) $row['zhr'] : null, - 'zhr_variable' => trim((string) ($row['zhr_variable'] ?? '')), - ]; + if ($isActive) { + $activeCount++; + } + + $shower['is_active_now'] = $isActive; + + if ($showMode === 'active' && !$isActive) { + continue; + } + + $showers[] = $shower; +} + +warmSolarLongitudeDateCache($showers, $displayYear, $displayTimezone); + +$topZhr = null; +foreach ($allShowers as $shower) { + if ($shower['zhr'] !== null) { + $topZhr = (int) $shower['zhr']; + break; } -} catch (Throwable $e) { - $starDataError = $e->getMessage(); } require __DIR__ . '/header.php'; - -$chartLocationName = (string) (($currentLocation['name'] ?? null) ?: 'Sternwarte Sonneberg'); -$chartLatitude = isset($currentLocation['latitude']) ? (float) $currentLocation['latitude'] : 50.3547; -$chartLongitude = isset($currentLocation['longitude']) ? (float) $currentLocation['longitude'] : 11.1900; -$chartElevation = isset($currentLocation['elevation']) && $currentLocation['elevation'] !== null ? (float) $currentLocation['elevation'] : 640.0; -$chartTimezone = (string) (($currentLocation['timezone'] ?? null) ?: 'Europe/Berlin'); - -$monthTime = $monthSkyTimes[$selectedMonth]; -$chartLocalTime = new DateTimeImmutable( - sprintf('%04d-%02d-15 %02d:%02d:00', $selectedYear, $selectedMonth, $monthTime['hour'], $monthTime['minute']), - new DateTimeZone($chartTimezone) -); -$chartUtcTime = $chartLocalTime->setTimezone(new DateTimeZone('UTC')); - -$pythonExecutable = PHP_OS_FAMILY === 'Windows' ? 'python' : '/usr/bin/python'; -$pythonScriptPath = __DIR__ . '/py/api.py'; -$pythonOutput = []; -$pythonExitCode = 0; -$planetErrorText = null; - -$pythonArgs = [ - $pythonScriptPath, - 'month_sky_context', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - (string) $monthTime['hour'], - (string) $monthTime['minute'], - $chartTimezone, -]; - -$pythonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $pythonArgs)) . ' 2>&1'; -exec($pythonCommand, $pythonOutput, $pythonExitCode); -$pythonRaw = trim(implode("\n", $pythonOutput)); -$pythonDecoded = json_decode($pythonRaw, true); - -if (is_array($pythonDecoded) && ($pythonDecoded['ok'] ?? false)) { - if (is_array($pythonDecoded['bodies']['moon'] ?? null)) { - $moon = $pythonDecoded['bodies']['moon']; - $moonData = [ - 'label' => (string) ($moon['label'] ?? 'Mond'), - 'az' => isset($moon['azimuth_deg']) ? (float) $moon['azimuth_deg'] : null, - 'alt' => isset($moon['altitude_deg']) ? (float) $moon['altitude_deg'] : null, - 'visible' => !empty($moon['visible']), - 'phaseFraction' => isset($moon['phase_fraction']) ? (float) $moon['phase_fraction'] : null, - 'waxing' => isset($moon['waxing']) ? (bool) $moon['waxing'] : null, - ]; - } - foreach (($pythonDecoded['bodies']['planets'] ?? []) as $planet) { - if (!is_array($planet)) { - continue; - } - $planetData[] = [ - 'key' => (string) ($planet['key'] ?? ''), - 'label' => (string) ($planet['label'] ?? ''), - 'az' => isset($planet['azimuth_deg']) ? (float) $planet['azimuth_deg'] : null, - 'alt' => isset($planet['altitude_deg']) ? (float) $planet['altitude_deg'] : null, - 'mag' => isset($planet['magnitude']) ? (float) $planet['magnitude'] : null, - 'visible' => !empty($planet['visible']), - ]; - } -} elseif ($pythonRaw !== '') { - $planetErrorText = $pythonRaw; -} - -$moonPhaseOutput = []; -$moonPhaseExitCode = 0; -$moonPhaseArgs = [ - $pythonScriptPath, - 'moon_phases_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$moonPhaseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPhaseArgs)) . ' 2>&1'; -exec($moonPhaseCommand, $moonPhaseOutput, $moonPhaseExitCode); -$moonPhaseRaw = trim(implode("\n", $moonPhaseOutput)); -$moonPhaseDecoded = json_decode($moonPhaseRaw, true); -if (is_array($moonPhaseDecoded) && ($moonPhaseDecoded['ok'] ?? false)) { - $moonPhaseEvents = is_array($moonPhaseDecoded['phases'] ?? null) ? $moonPhaseDecoded['phases'] : []; -} - -$goldenHandleOutput = []; -$goldenHandleExitCode = 0; -$goldenHandleArgs = [ - $pythonScriptPath, - 'golden_handle_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$goldenHandleCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $goldenHandleArgs)) . ' 2>&1'; -exec($goldenHandleCommand, $goldenHandleOutput, $goldenHandleExitCode); -$goldenHandleRaw = trim(implode("\n", $goldenHandleOutput)); -$goldenHandleDecoded = json_decode($goldenHandleRaw, true); -if (is_array($goldenHandleDecoded) && ($goldenHandleDecoded['ok'] ?? false)) { - $goldenHandleEvents = is_array($goldenHandleDecoded['events'] ?? null) ? $goldenHandleDecoded['events'] : []; -} - -$seasonOutput = []; -$seasonExitCode = 0; -$seasonArgs = [ - $pythonScriptPath, - 'season_changes_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$seasonCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $seasonArgs)) . ' 2>&1'; -exec($seasonCommand, $seasonOutput, $seasonExitCode); -$seasonRaw = trim(implode("\n", $seasonOutput)); -$seasonDecoded = json_decode($seasonRaw, true); -if (is_array($seasonDecoded) && ($seasonDecoded['ok'] ?? false)) { - $seasonChanges = is_array($seasonDecoded['changes'] ?? null) ? $seasonDecoded['changes'] : []; -} - -$timeChangeOutput = []; -$timeChangeExitCode = 0; -$timeChangeArgs = [ - $pythonScriptPath, - 'time_changes_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$timeChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $timeChangeArgs)) . ' 2>&1'; -exec($timeChangeCommand, $timeChangeOutput, $timeChangeExitCode); -$timeChangeRaw = trim(implode("\n", $timeChangeOutput)); -$timeChangeDecoded = json_decode($timeChangeRaw, true); -if (is_array($timeChangeDecoded) && ($timeChangeDecoded['ok'] ?? false)) { - $timeChanges = is_array($timeChangeDecoded['events'] ?? null) ? $timeChangeDecoded['events'] : []; -} - -$eclipseOutput = []; -$eclipseExitCode = 0; -$eclipseArgs = [ - $pythonScriptPath, - 'eclipses_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$eclipseCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $eclipseArgs)) . ' 2>&1'; -exec($eclipseCommand, $eclipseOutput, $eclipseExitCode); -$eclipseRaw = trim(implode("\n", $eclipseOutput)); -$eclipseDecoded = json_decode($eclipseRaw, true); -if (is_array($eclipseDecoded) && ($eclipseDecoded['ok'] ?? false)) { - $eclipseEvents = is_array($eclipseDecoded['events'] ?? null) ? $eclipseDecoded['events'] : []; -} - -$moonApsisOutput = []; -$moonApsisExitCode = 0; -$moonApsisArgs = [ - $pythonScriptPath, - 'moon_apsides_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$moonApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonApsisArgs)) . ' 2>&1'; -exec($moonApsisCommand, $moonApsisOutput, $moonApsisExitCode); -$moonApsisRaw = trim(implode("\n", $moonApsisOutput)); -$moonApsisDecoded = json_decode($moonApsisRaw, true); -if (is_array($moonApsisDecoded) && ($moonApsisDecoded['ok'] ?? false)) { - $moonApsisEvents = is_array($moonApsisDecoded['events'] ?? null) ? $moonApsisDecoded['events'] : []; -} - -$sunApsisOutput = []; -$sunApsisExitCode = 0; -$sunApsisArgs = [ - $pythonScriptPath, - 'sun_apsides_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$sunApsisCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $sunApsisArgs)) . ' 2>&1'; -exec($sunApsisCommand, $sunApsisOutput, $sunApsisExitCode); -$sunApsisRaw = trim(implode("\n", $sunApsisOutput)); -$sunApsisDecoded = json_decode($sunApsisRaw, true); -if (is_array($sunApsisDecoded) && ($sunApsisDecoded['ok'] ?? false)) { - $sunApsisEvents = is_array($sunApsisDecoded['events'] ?? null) ? $sunApsisDecoded['events'] : []; -} - -$elongationOutput = []; -$elongationExitCode = 0; -$elongationArgs = [ - $pythonScriptPath, - 'inner_planet_elongations_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$elongationCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $elongationArgs)) . ' 2>&1'; -exec($elongationCommand, $elongationOutput, $elongationExitCode); -$elongationRaw = trim(implode("\n", $elongationOutput)); -$elongationDecoded = json_decode($elongationRaw, true); -if (is_array($elongationDecoded) && ($elongationDecoded['ok'] ?? false)) { - $innerPlanetElongationEvents = is_array($elongationDecoded['events'] ?? null) ? $elongationDecoded['events'] : []; -} - -$venusPeakMagnitudeOutput = []; -$venusPeakMagnitudeExitCode = 0; -$venusPeakMagnitudeArgs = [ - $pythonScriptPath, - 'venus_peak_magnitude_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$venusPeakMagnitudeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $venusPeakMagnitudeArgs)) . ' 2>&1'; -exec($venusPeakMagnitudeCommand, $venusPeakMagnitudeOutput, $venusPeakMagnitudeExitCode); -$venusPeakMagnitudeRaw = trim(implode("\n", $venusPeakMagnitudeOutput)); -$venusPeakMagnitudeDecoded = json_decode($venusPeakMagnitudeRaw, true); -if (is_array($venusPeakMagnitudeDecoded) && ($venusPeakMagnitudeDecoded['ok'] ?? false)) { - $venusPeakMagnitudeEvents = is_array($venusPeakMagnitudeDecoded['events'] ?? null) ? $venusPeakMagnitudeDecoded['events'] : []; -} - -$outerPlanetOutput = []; -$outerPlanetExitCode = 0; -$outerPlanetArgs = [ - $pythonScriptPath, - 'outer_planet_events_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$outerPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $outerPlanetArgs)) . ' 2>&1'; -exec($outerPlanetCommand, $outerPlanetOutput, $outerPlanetExitCode); -$outerPlanetRaw = trim(implode("\n", $outerPlanetOutput)); -$outerPlanetDecoded = json_decode($outerPlanetRaw, true); -if (is_array($outerPlanetDecoded) && ($outerPlanetDecoded['ok'] ?? false)) { - $outerPlanetEvents = is_array($outerPlanetDecoded['events'] ?? null) ? $outerPlanetDecoded['events'] : []; -} - -$outerPlanetStationOutput = []; -$outerPlanetStationExitCode = 0; -$outerPlanetStationArgs = [ - $pythonScriptPath, - 'outer_planet_stations_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$outerPlanetStationCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $outerPlanetStationArgs)) . ' 2>&1'; -exec($outerPlanetStationCommand, $outerPlanetStationOutput, $outerPlanetStationExitCode); -$outerPlanetStationRaw = trim(implode("\n", $outerPlanetStationOutput)); -$outerPlanetStationDecoded = json_decode($outerPlanetStationRaw, true); -if (is_array($outerPlanetStationDecoded) && ($outerPlanetStationDecoded['ok'] ?? false)) { - $outerPlanetStationEvents = is_array($outerPlanetStationDecoded['events'] ?? null) ? $outerPlanetStationDecoded['events'] : []; -} - -$jupiterMoonSideOutput = []; -$jupiterMoonSideExitCode = 0; -$jupiterMoonSideArgs = [ - $pythonScriptPath, - 'jupiter_moons_one_side_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$jupiterMoonSideCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $jupiterMoonSideArgs)) . ' 2>&1'; -exec($jupiterMoonSideCommand, $jupiterMoonSideOutput, $jupiterMoonSideExitCode); -$jupiterMoonSideRaw = trim(implode("\n", $jupiterMoonSideOutput)); -$jupiterMoonSideDecoded = json_decode($jupiterMoonSideRaw, true); -if (is_array($jupiterMoonSideDecoded) && ($jupiterMoonSideDecoded['ok'] ?? false)) { - $jupiterMoonSideEvents = is_array($jupiterMoonSideDecoded['events'] ?? null) ? $jupiterMoonSideDecoded['events'] : []; -} - -$moonPlanetOutput = []; -$moonPlanetExitCode = 0; -$moonPlanetArgs = [ - $pythonScriptPath, - 'moon_planet_approaches', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - (string) $moonPlanetMaxSeparationDeg, -]; -$moonPlanetCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonPlanetArgs)) . ' 2>&1'; -exec($moonPlanetCommand, $moonPlanetOutput, $moonPlanetExitCode); -$moonPlanetRaw = trim(implode("\n", $moonPlanetOutput)); -$moonPlanetDecoded = json_decode($moonPlanetRaw, true); -if (is_array($moonPlanetDecoded) && ($moonPlanetDecoded['ok'] ?? false)) { - $moonPlanetApproaches = is_array($moonPlanetDecoded['approaches'] ?? null) ? $moonPlanetDecoded['approaches'] : []; -} - -$moonDeepSkyOutput = []; -$moonDeepSkyExitCode = 0; -$moonDeepSkyArgs = [ - $pythonScriptPath, - 'moon_deep_sky_approaches_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - (string) $moonDeepSkyMaxSeparationDeg, -]; -$moonDeepSkyCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonDeepSkyArgs)) . ' 2>&1'; -exec($moonDeepSkyCommand, $moonDeepSkyOutput, $moonDeepSkyExitCode); -$moonDeepSkyRaw = trim(implode("\n", $moonDeepSkyOutput)); -$moonDeepSkyDecoded = json_decode($moonDeepSkyRaw, true); -if (is_array($moonDeepSkyDecoded) && ($moonDeepSkyDecoded['ok'] ?? false)) { - $moonDeepSkyApproaches = is_array($moonDeepSkyDecoded['approaches'] ?? null) ? $moonDeepSkyDecoded['approaches'] : []; -} - -$planetBrightStarOutput = []; -$planetBrightStarExitCode = 0; -$planetBrightStarArgs = [ - $pythonScriptPath, - 'planet_bright_star_approaches_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - (string) $planetBrightStarMaxSeparationDeg, -]; -$planetBrightStarCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetBrightStarArgs)) . ' 2>&1'; -exec($planetBrightStarCommand, $planetBrightStarOutput, $planetBrightStarExitCode); -$planetBrightStarRaw = trim(implode("\n", $planetBrightStarOutput)); -$planetBrightStarDecoded = json_decode($planetBrightStarRaw, true); -if (is_array($planetBrightStarDecoded) && ($planetBrightStarDecoded['ok'] ?? false)) { - $planetBrightStarApproaches = is_array($planetBrightStarDecoded['approaches'] ?? null) ? $planetBrightStarDecoded['approaches'] : []; -} - -$planetConjunctionOutput = []; -$planetConjunctionExitCode = 0; -$planetConjunctionArgs = [ - $pythonScriptPath, - 'planet_conjunctions_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - (string) $planetConjunctionMaxSeparationDeg, -]; -$planetConjunctionCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConjunctionArgs)) . ' 2>&1'; -exec($planetConjunctionCommand, $planetConjunctionOutput, $planetConjunctionExitCode); -$planetConjunctionRaw = trim(implode("\n", $planetConjunctionOutput)); -$planetConjunctionDecoded = json_decode($planetConjunctionRaw, true); -if (is_array($planetConjunctionDecoded) && ($planetConjunctionDecoded['ok'] ?? false)) { - $planetConjunctions = is_array($planetConjunctionDecoded['conjunctions'] ?? null) ? $planetConjunctionDecoded['conjunctions'] : []; -} - -$mercuryGoodVisibilityOutput = []; -$mercuryGoodVisibilityExitCode = 0; -$mercuryGoodVisibilityArgs = [ - $pythonScriptPath, - 'mercury_good_visibility_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - (string) $mercuryGoodVisibilitySampleOffsetMinutes, - (string) $mercuryGoodVisibilityMinAltitudeDeg, - (string) $mercuryGoodVisibilityMaxSunAltitudeDeg, - (string) $mercuryGoodVisibilityMinConsecutiveDays, -]; -$mercuryGoodVisibilityCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $mercuryGoodVisibilityArgs)) . ' 2>&1'; -exec($mercuryGoodVisibilityCommand, $mercuryGoodVisibilityOutput, $mercuryGoodVisibilityExitCode); -$mercuryGoodVisibilityRaw = trim(implode("\n", $mercuryGoodVisibilityOutput)); -$mercuryGoodVisibilityDecoded = json_decode($mercuryGoodVisibilityRaw, true); -if (is_array($mercuryGoodVisibilityDecoded) && ($mercuryGoodVisibilityDecoded['ok'] ?? false)) { - $mercuryGoodVisibilityEvents = is_array($mercuryGoodVisibilityDecoded['events'] ?? null) ? $mercuryGoodVisibilityDecoded['events'] : []; -} - -$planetParadeOutput = []; -$planetParadeExitCode = 0; -$planetParadeArgs = [ - $pythonScriptPath, - 'planet_parades_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - (string) $planetParadeSampleOffsetMinutes, - (string) $planetParadeMinAltitudeDeg, - (string) $planetParadeMaxSunAltitudeDeg, - (string) $planetParadeMaxAzimuthSpanDeg, - (string) $planetParadeMinPlanetCount, - (string) $planetParadeMinConsecutiveDays, -]; -$planetParadeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetParadeArgs)) . ' 2>&1'; -exec($planetParadeCommand, $planetParadeOutput, $planetParadeExitCode); -$planetParadeRaw = trim(implode("\n", $planetParadeOutput)); -$planetParadeDecoded = json_decode($planetParadeRaw, true); -if (is_array($planetParadeDecoded) && ($planetParadeDecoded['ok'] ?? false)) { - $planetParadeEvents = is_array($planetParadeDecoded['events'] ?? null) ? $planetParadeDecoded['events'] : []; -} - -$planetConstellationChangeOutput = []; -$planetConstellationChangeExitCode = 0; -$planetConstellationChangeArgs = [ - $pythonScriptPath, - 'planet_constellation_changes_for_month', - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, -]; -$planetConstellationChangeCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $planetConstellationChangeArgs)) . ' 2>&1'; -exec($planetConstellationChangeCommand, $planetConstellationChangeOutput, $planetConstellationChangeExitCode); -$planetConstellationChangeRaw = trim(implode("\n", $planetConstellationChangeOutput)); -$planetConstellationChangeDecoded = json_decode($planetConstellationChangeRaw, true); -if (is_array($planetConstellationChangeDecoded) && ($planetConstellationChangeDecoded['ok'] ?? false)) { - $planetConstellationChangeEvents = is_array($planetConstellationChangeDecoded['events'] ?? null) ? $planetConstellationChangeDecoded['events'] : []; -} - -$moonOccultationStars = []; -try { - if (isset($pdo)) { - $stmtOccultStars = $pdo->prepare( - "SELECT hip, proper, bayer, con, mag, rarad, decrad, dist - FROM star_hipparcos - WHERE rarad IS NOT NULL - AND decrad IS NOT NULL - AND mag IS NOT NULL - AND mag <= :max_mag - ORDER BY mag ASC, proper ASC, bayer ASC, hip ASC" - ); - $stmtOccultStars->execute([':max_mag' => $moonOccultationMaxMag]); - - foreach ($stmtOccultStars->fetchAll() as $row) { - $moonOccultationStars[] = [ - 'hip' => isset($row['hip']) ? (int) $row['hip'] : 0, - 'label' => buildMonthOccultationStarLabel($row), - 'constellation' => trim((string) ($row['con'] ?? '')), - 'mag' => isset($row['mag']) ? (float) $row['mag'] : 99.0, - 'ra' => isset($row['rarad']) ? (float) $row['rarad'] * (12 / M_PI) : 0.0, - 'dec' => isset($row['decrad']) ? (float) $row['decrad'] * (180 / M_PI) : 0.0, - 'distLy' => isset($row['dist']) && $row['dist'] !== null && (float) $row['dist'] > 0.0 - ? (float) $row['dist'] * 3.26156 - : 1000.0, - ]; - } - } -} catch (Throwable $e) { -} - -if ($moonOccultationStars) { - $tempOccultationFile = tempnam(sys_get_temp_dir(), 'month_occult_'); - if ($tempOccultationFile !== false) { - try { - file_put_contents($tempOccultationFile, json_encode($moonOccultationStars, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR)); - $moonOccultOutput = []; - $moonOccultExitCode = 0; - $moonOccultArgs = [ - $pythonScriptPath, - 'moon_star_occultations_for_month', - (string) $chartLatitude, - (string) $chartLongitude, - (string) $chartElevation, - (string) $selectedYear, - (string) $selectedMonth, - $chartTimezone, - $tempOccultationFile, - ]; - $moonOccultCommand = $pythonExecutable . ' ' . implode(' ', array_map('escapeshellarg', $moonOccultArgs)) . ' 2>&1'; - exec($moonOccultCommand, $moonOccultOutput, $moonOccultExitCode); - $moonOccultRaw = trim(implode("\n", $moonOccultOutput)); - $moonOccultDecoded = json_decode($moonOccultRaw, true); - if (is_array($moonOccultDecoded) && ($moonOccultDecoded['ok'] ?? false)) { - $moonOccultations = is_array($moonOccultDecoded['results'] ?? null) ? $moonOccultDecoded['results'] : []; - } - } catch (Throwable $e) { - } finally { - if (is_file($tempOccultationFile)) { - @unlink($tempOccultationFile); - } - } - } -} - -foreach ($moonPhaseEvents as $phaseEvent) { - if (!is_array($phaseEvent)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($phaseEvent['label'] ?? ''), - 'date' => (string) ($phaseEvent['local_date'] ?? ''), - 'time' => (string) ($phaseEvent['local_time'] ?? ''), - 'local_iso' => (string) ($phaseEvent['local_iso'] ?? ''), - 'type' => 'moon_phase', - ]; -} - -foreach ($goldenHandleEvents as $goldenHandleEvent) { - if (!is_array($goldenHandleEvent)) { - continue; - } - - $peakDate = (string) ($goldenHandleEvent['peak_local_date'] ?? ''); - $peakTime = (string) ($goldenHandleEvent['peak_local_time'] ?? ''); - $goldenHandleLabel = 'Goldener Henkel am Mond'; - - $monthEventList[] = [ - 'event' => $goldenHandleLabel, - 'date' => $peakDate !== '' ? $peakDate : (string) ($goldenHandleEvent['local_date'] ?? ''), - 'time' => $peakTime !== '' ? $peakTime : (string) ($goldenHandleEvent['local_time'] ?? ''), - 'local_iso' => (string) ($goldenHandleEvent['peak_local_iso'] ?? $goldenHandleEvent['local_iso'] ?? ''), - 'type' => 'golden_handle', - ]; -} - -foreach ($seasonChanges as $seasonChange) { - if (!is_array($seasonChange)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($seasonChange['label'] ?? 'Jahreszeitenwechsel'), - 'date' => (string) ($seasonChange['local_date'] ?? ''), - 'time' => (string) ($seasonChange['local_time'] ?? ''), - 'local_iso' => (string) ($seasonChange['local_iso'] ?? ''), - 'type' => 'season_change', - ]; -} - -foreach ($timeChanges as $timeChange) { - if (!is_array($timeChange)) { - continue; - } - - $timeChangeKind = (string) ($timeChange['kind'] ?? ''); - $timeChangeLabel = $timeChangeKind === 'dst_start' - ? 'Beginn der Sommerzeit (Uhren von 02:00 auf 03:00 Uhr vorstellen)' - : ($timeChangeKind === 'dst_end' - ? 'Beginn der Normalzeit (Uhren von 03:00 auf 02:00 Uhr zurueckstellen)' - : 'Zeitumstellung'); - $timeChangeTime = $timeChangeKind === 'dst_start' - ? '02:00' - : ($timeChangeKind === 'dst_end' ? '03:00' : (string) ($timeChange['local_time'] ?? '')); - - $monthEventList[] = [ - 'event' => $timeChangeLabel, - 'date' => (string) ($timeChange['local_date'] ?? ''), - 'time' => $timeChangeTime, - 'local_iso' => (string) ($timeChange['local_iso'] ?? ''), - 'type' => 'time_change', - ]; -} - -foreach ($eclipseEvents as $eclipseEvent) { - if (!is_array($eclipseEvent)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($eclipseEvent['label'] ?? 'Finsternis'), - 'date' => (string) ($eclipseEvent['local_date'] ?? ''), - 'time' => (string) ($eclipseEvent['local_time'] ?? ''), - 'local_iso' => (string) ($eclipseEvent['local_iso'] ?? ''), - 'type' => (string) ($eclipseEvent['category'] ?? 'eclipse'), - ]; -} - -foreach ($moonApsisEvents as $moonApsisEvent) { - if (!is_array($moonApsisEvent)) { - continue; - } - - $moonApsisKind = (string) ($moonApsisEvent['kind'] ?? ''); - $distanceText = isset($moonApsisEvent['distance_km']) - ? number_format((float) $moonApsisEvent['distance_km'], 0, ',', '.') . ' km' - : ''; - $moonApsisLabel = $moonApsisKind === 'perigee' - ? 'Mond in Erdnähe (Perigäum)' - : ($moonApsisKind === 'apogee' ? 'Mond in Erdferne (Apogäum)' : (string) ($moonApsisEvent['label'] ?? 'Mondabstand')); - - $monthEventList[] = [ - 'event' => trim($moonApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), - 'date' => (string) ($moonApsisEvent['local_date'] ?? ''), - 'time' => (string) ($moonApsisEvent['local_time'] ?? ''), - 'local_iso' => (string) ($moonApsisEvent['local_iso'] ?? ''), - 'type' => 'moon_apsis', - ]; -} - -foreach ($sunApsisEvents as $sunApsisEvent) { - if (!is_array($sunApsisEvent)) { - continue; - } - - $sunApsisKind = (string) ($sunApsisEvent['kind'] ?? ''); - $distanceText = isset($sunApsisEvent['distance_km']) - ? number_format((float) $sunApsisEvent['distance_km'], 0, ',', '.') . ' km' - : ''; - $sunApsisLabel = $sunApsisKind === 'perihelion' - ? 'Erde erreicht sonnennächsten Punkt (Perihel)' - : ($sunApsisKind === 'aphelion' ? 'Erde erreicht sonnenfernsten Punkt (Aphel)' : (string) ($sunApsisEvent['label'] ?? 'Sonnenabstand')); - - $monthEventList[] = [ - 'event' => trim($sunApsisLabel . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), - 'date' => (string) ($sunApsisEvent['local_date'] ?? ''), - 'time' => (string) ($sunApsisEvent['local_time'] ?? ''), - 'local_iso' => (string) ($sunApsisEvent['local_iso'] ?? ''), - 'type' => 'sun_apsis', - ]; -} - -foreach ($innerPlanetElongationEvents as $elongationEvent) { - if (!is_array($elongationEvent)) { - continue; - } - - $elongationPlanet = (string) ($elongationEvent['planet_label'] ?? 'Planet'); - $elongationKind = (string) ($elongationEvent['kind'] ?? ''); - $elongationText = isset($elongationEvent['elongation_deg']) - ? number_format((float) $elongationEvent['elongation_deg'], 2, ',', '') . ' Grad' - : ''; - $elongationLabel = $elongationKind === 'western' - ? $elongationPlanet . ' größte westliche Elongation' - : ($elongationKind === 'eastern' - ? $elongationPlanet . ' größte östliche Elongation' - : (string) ($elongationEvent['label'] ?? 'Elongation')); - - $monthEventList[] = [ - 'event' => trim($elongationLabel . ($elongationText !== '' ? ' (' . $elongationText . ')' : '')), - 'date' => (string) ($elongationEvent['local_date'] ?? ''), - 'time' => (string) ($elongationEvent['local_time'] ?? ''), - 'local_iso' => (string) ($elongationEvent['local_iso'] ?? ''), - 'type' => 'planet_elongation', - ]; -} - -foreach ($venusPeakMagnitudeEvents as $venusPeakMagnitudeEvent) { - if (!is_array($venusPeakMagnitudeEvent)) { - continue; - } - - $magnitudeText = isset($venusPeakMagnitudeEvent['mag']) - ? number_format((float) $venusPeakMagnitudeEvent['mag'], 1, ',', '') . ' mag' - : ''; - - $monthEventList[] = [ - 'event' => trim('Venus in groesster Helligkeit' . ($magnitudeText !== '' ? ' (' . $magnitudeText . ')' : '')), - 'date' => (string) ($venusPeakMagnitudeEvent['local_date'] ?? ''), - 'time' => (string) ($venusPeakMagnitudeEvent['local_time'] ?? ''), - 'local_iso' => (string) ($venusPeakMagnitudeEvent['local_iso'] ?? ''), - 'type' => 'venus_peak_magnitude', - ]; -} - -foreach ($outerPlanetEvents as $outerPlanetEvent) { - if (!is_array($outerPlanetEvent)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($outerPlanetEvent['label'] ?? 'Planetenereignis'), - 'date' => (string) ($outerPlanetEvent['local_date'] ?? ''), - 'time' => (string) ($outerPlanetEvent['local_time'] ?? ''), - 'local_iso' => (string) ($outerPlanetEvent['local_iso'] ?? ''), - 'type' => 'outer_planet_event', - ]; -} - -foreach ($outerPlanetStationEvents as $outerPlanetStationEvent) { - if (!is_array($outerPlanetStationEvent)) { - continue; - } - - $stationPlanet = (string) ($outerPlanetStationEvent['planet_label'] ?? 'Planet'); - $stationKind = (string) ($outerPlanetStationEvent['kind'] ?? ''); - $stationLabel = $stationKind === 'retrograde' - ? $stationPlanet . ' stationär, dann rückläufig' - : ($stationKind === 'prograde' - ? $stationPlanet . ' stationär, dann rechtläufig' - : (string) ($outerPlanetStationEvent['label'] ?? 'Stationärer Punkt')); - - $monthEventList[] = [ - 'event' => $stationLabel, - 'date' => (string) ($outerPlanetStationEvent['local_date'] ?? ''), - 'time' => (string) ($outerPlanetStationEvent['local_time'] ?? ''), - 'local_iso' => (string) ($outerPlanetStationEvent['local_iso'] ?? ''), - 'type' => 'outer_planet_station', - ]; -} - -foreach ($jupiterMoonSideEvents as $jupiterMoonSideEvent) { - if (!is_array($jupiterMoonSideEvent)) { - continue; - } - - $durationLabel = trim((string) ($jupiterMoonSideEvent['duration_label'] ?? '')); - $eventLabel = trim((string) ($jupiterMoonSideEvent['label'] ?? 'Alle 4 Jupitermonde auf einer Seite von Jupiter')); - if ($durationLabel !== '') { - $eventLabel .= ' (' . $durationLabel . ')'; - } - - $monthEventList[] = [ - 'event' => $eventLabel, - 'date' => (string) ($jupiterMoonSideEvent['local_date'] ?? ''), - 'time' => (string) ($jupiterMoonSideEvent['local_time'] ?? ''), - 'local_iso' => (string) ($jupiterMoonSideEvent['local_iso'] ?? ''), - 'end_local_iso' => (string) ($jupiterMoonSideEvent['end_local_iso'] ?? ''), - 'duration_minutes' => isset($jupiterMoonSideEvent['duration_minutes']) ? (int) $jupiterMoonSideEvent['duration_minutes'] : null, - 'type' => 'jupiter_moons_one_side', - ]; -} - -foreach ($moonPlanetApproaches as $approachEvent) { - if (!is_array($approachEvent)) { - continue; - } - - $distanceText = isset($approachEvent['separation_deg']) - ? number_format((float) $approachEvent['separation_deg'], 2, ',', '') . ' Grad' - : ''; - - $monthEventList[] = [ - 'event' => trim((string) ($approachEvent['label'] ?? 'Mond nahe Planet') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), - 'date' => (string) ($approachEvent['local_date'] ?? ''), - 'time' => (string) ($approachEvent['local_time'] ?? ''), - 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), - 'type' => 'moon_planet', - ]; -} - -foreach ($moonDeepSkyApproaches as $approachEvent) { - if (!is_array($approachEvent)) { - continue; - } - - $distanceText = isset($approachEvent['separation_deg']) - ? number_format((float) ($approachEvent['separation_deg']), 2, ',', '') . ' Grad' - : ''; - - $monthEventList[] = [ - 'event' => trim((string) ($approachEvent['label'] ?? 'Mond nahe Sternhaufen') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), - 'date' => (string) ($approachEvent['local_date'] ?? ''), - 'time' => (string) ($approachEvent['local_time'] ?? ''), - 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), - 'type' => 'moon_deep_sky', - ]; -} - -foreach ($planetBrightStarApproaches as $approachEvent) { - if (!is_array($approachEvent)) { - continue; - } - - $distanceText = isset($approachEvent['separation_deg']) - ? number_format((float) ($approachEvent['separation_deg']), 2, ',', '') . ' Grad' - : ''; - - $monthEventList[] = [ - 'event' => trim((string) ($approachEvent['label'] ?? 'Planet nahe hellem Stern') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), - 'date' => (string) ($approachEvent['local_date'] ?? ''), - 'time' => (string) ($approachEvent['local_time'] ?? ''), - 'local_iso' => (string) ($approachEvent['local_iso'] ?? ''), - 'type' => 'planet_bright_star', - ]; -} - -foreach ($planetConjunctions as $conjunctionEvent) { - if (!is_array($conjunctionEvent)) { - continue; - } - - $distanceText = isset($conjunctionEvent['separation_deg']) - ? number_format((float) $conjunctionEvent['separation_deg'], 2, ',', '') . ' Grad' - : ''; - - $monthEventList[] = [ - 'event' => trim((string) ($conjunctionEvent['label'] ?? 'Planetenkonjunktion') . ($distanceText !== '' ? ' (' . $distanceText . ')' : '')), - 'date' => (string) ($conjunctionEvent['local_date'] ?? ''), - 'time' => (string) ($conjunctionEvent['local_time'] ?? ''), - 'local_iso' => (string) ($conjunctionEvent['local_iso'] ?? ''), - 'type' => 'planet_conjunction', - ]; -} - -foreach ($mercuryGoodVisibilityEvents as $visibilityEvent) { - if (!is_array($visibilityEvent)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($visibilityEvent['label'] ?? 'Gute Merkur-Sichtbarkeit'), - 'date' => (string) ($visibilityEvent['local_date'] ?? ''), - 'time' => (string) ($visibilityEvent['local_time'] ?? ''), - 'local_iso' => (string) ($visibilityEvent['local_iso'] ?? ''), - 'end_local_iso' => (string) ($visibilityEvent['end_local_iso'] ?? ''), - 'duration_minutes' => isset($visibilityEvent['duration_minutes']) ? (int) $visibilityEvent['duration_minutes'] : null, - 'type' => 'mercury_good_visibility', - ]; -} - -foreach ($planetParadeEvents as $paradeEvent) { - if (!is_array($paradeEvent)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($paradeEvent['label'] ?? 'Planetenparade'), - 'date' => (string) ($paradeEvent['local_date'] ?? ''), - 'time' => (string) ($paradeEvent['local_time'] ?? ''), - 'local_iso' => (string) ($paradeEvent['local_iso'] ?? ''), - 'end_local_iso' => (string) ($paradeEvent['end_local_iso'] ?? ''), - 'duration_minutes' => isset($paradeEvent['duration_minutes']) ? (int) $paradeEvent['duration_minutes'] : null, - 'type' => 'planet_parade', - ]; -} - -foreach ($planetConstellationChangeEvents as $changeEvent) { - if (!is_array($changeEvent)) { - continue; - } - - $monthEventList[] = [ - 'event' => (string) ($changeEvent['label'] ?? 'Planet wechselt Sternbild'), - 'date' => (string) ($changeEvent['local_date'] ?? ''), - 'time' => (string) ($changeEvent['local_time'] ?? ''), - 'local_iso' => (string) ($changeEvent['local_iso'] ?? ''), - 'type' => 'planet_constellation_change', - ]; -} - -foreach ($moonOccultations as $occultationEvent) { - if (!is_array($occultationEvent)) { - continue; - } - - $maximum = is_array($occultationEvent['maximum'] ?? null) ? $occultationEvent['maximum'] : null; - $star = is_array($occultationEvent['star'] ?? null) ? $occultationEvent['star'] : null; - if ($maximum === null || $star === null) { - continue; - } - - $starLabel = (string) ($star['label'] ?? 'Stern'); - $starMag = isset($star['mag']) ? number_format((float) $star['mag'], 1, ',', '') : ''; - $monthEventList[] = [ - 'event' => 'Mond bedeckt ' . $starLabel . ($starMag !== '' ? ' (' . $starMag . ' mag)' : ''), - 'date' => (string) ($maximum['local_date'] ?? ''), - 'time' => (string) ($maximum['local_time'] ?? ''), - 'local_iso' => (string) ($maximum['local_iso'] ?? ''), - 'type' => 'moon_occultation', - ]; -} - -$monthStartLocal = new DateTimeImmutable( - sprintf('%04d-%02d-01 00:00:00', $selectedYear, $selectedMonth), - new DateTimeZone($chartTimezone) -); -$monthEndLocal = $monthStartLocal->modify('first day of next month'); - -foreach ($meteorShowerPeaks as $meteorShowerPeak) { - if (!monthForecastRadiantIsVisible($chartLatitude, $meteorShowerPeak['radiant_delta_deg'] ?? null)) { - continue; - } - - $peakSol = isset($meteorShowerPeak['peak_sol']) ? (float) $meteorShowerPeak['peak_sol'] : null; - if ($peakSol === null) { - continue; - } - - $peakDateTime = findMonthForecastSolarLongitudeCrossing( - $monthStartLocal, - $monthEndLocal, - $peakSol, - new DateTimeZone($chartTimezone) - ); - if (!$peakDateTime instanceof DateTimeImmutable) { - continue; - } - - $designation = trim((string) ($meteorShowerPeak['designation'] ?? '')); - $showerCode = trim((string) ($meteorShowerPeak['shower_code'] ?? '')); - $zhr = $meteorShowerPeak['zhr'] ?? null; - $zhrVariable = trim((string) ($meteorShowerPeak['zhr_variable'] ?? '')); - $rateText = ''; - - if (is_int($zhr) && $zhr >= 0) { - $rateText = 'ZHR ca. ' . $zhr; - } elseif ($zhrVariable !== '') { - $rateText = 'ZHR ca. ' . $zhrVariable; - } - - $labelParts = []; - $labelParts[] = 'Maximum ' . ($designation !== '' ? $designation : 'Meteorstrom'); - if ($showerCode !== '') { - $labelParts[] = $showerCode; - } - if ($rateText !== '') { - $labelParts[] = $rateText; - } - - $monthEventList[] = [ - 'event' => implode(' - ', $labelParts), - 'date' => $peakDateTime->format('d.m.Y'), - 'time' => $peakDateTime->format('H:i'), - 'local_iso' => $peakDateTime->format(DateTimeInterface::ATOM), - 'type' => 'meteor_shower_peak', - ]; -} - -usort( - $monthEventList, - static fn (array $a, array $b): int => strcmp((string) ($a['local_iso'] ?? ''), (string) ($b['local_iso'] ?? '')) -); - -$observationTipInsertSql = buildObservationTipInsertSql($monthEventList, new DateTimeZone($chartTimezone)); -$observationTipCalendarIcs = buildObservationTipCalendarIcs($monthEventList, new DateTimeZone($chartTimezone)); - -$chartConfig = [ - 'monthName' => $months[$selectedMonth], - 'year' => $selectedYear, - 'month' => $selectedMonth, - 'location' => [ - 'name' => $chartLocationName, - 'latitude' => $chartLatitude, - 'longitude' => $chartLongitude, - 'elevation' => $chartElevation, - 'timezone' => $chartTimezone, - ], - 'localTimeLabel' => $chartLocalTime->format('d.m.Y H:i') . ' ' . $chartTimezone, - 'utcTimeIso' => $chartUtcTime->format(DateTimeInterface::ATOM), - 'monthSkyHour' => $monthTime['hour'], - 'monthSkyMinute' => $monthTime['minute'], - 'starLimit' => $starDisplayMagnitudeLimit, - 'starLabelLimit' => $starLabelMagnitudeLimit, - 'messierLimit' => $messierDisplayMagnitudeLimit, -]; - -$chartConfigJson = json_encode($chartConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$starDataJson = json_encode($starData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$lineStarDataJson = json_encode($lineStarData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$constellationLinesJson = json_encode($constellationLines, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$messierDataJson = json_encode($messierData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$planetDataJson = json_encode($planetData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$moonDataJson = json_encode($moonData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$moonPhaseEventsJson = json_encode($moonPhaseEvents, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$starDataErrorJson = json_encode($starDataError, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$observationTipCalendarIcsJson = json_encode($observationTipCalendarIcs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); -$observationTipCalendarFilenameJson = json_encode(sprintf('astrotools-monatsereignisse-%04d-%02d.ics', $selectedYear, $selectedMonth), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?> -
-
-
-
-

Zeitraum waehlen

-

Die Sternkarte wird fuer den 15. des gewaehlten Monats mit der festen Monatsuhrzeit erzeugt.

-
+ 'Importierte Meteorstroeme', 'value' => (string) count($allShowers)], + ['label' => 'Davon aktuell aktiv', 'value' => (string) $activeCount], + ['label' => 'Hoechste ZHR', 'value' => $topZhr !== null ? (string) $topZhr : '-'], + ['label' => 'Aktuelle solare Laenge', 'value' => number_format($todaySolarLongitude, 2, ',', '.') . '°'], + ['label' => 'Datumsumrechnung', 'value' => (string) $displayYear], +]; +?> - - - - -
- -
-
- -
-
-
-

Zenitprojektion

-

- , - -

-
-
- -
- -
- - -
- -
-
-
-

Südhimmel

-

- , - -

-
-
- -
- -
- - -
- -
-
-
-

Mondphasen Im

-

Eine Monatszeile vom ersten bis zum letzten Tag mit der Mondtextur aus moon4geocron.png.

-
-
- -
-
- Mondphasen des Monats in einer Zeile -
-
- - -
- -
-
-
-

Ereignisse

-

Chronologische Monatsliste fuer

-
-
- - - - - - -
-
- -
+ +
+
- +
+
+
+
+ + +
+
+ + +
+
+
+ + Zuruecksetzen +
+
+
+ +
+

Liste der Meteorstroeme

+

Die Gradangaben in Aktivitaet und Maximum sind Werte der solaren Laenge. Zusaetzlich wird dafuer ein Kalenderdatum im Jahr angezeigt.

+ +

Keine Meteorstroeme fuer diese Auswahl gefunden.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StatusCodeIAUBezeichnungZHRAktivitaet (solare Laenge)Maximum (solare Laenge)RadiantvgMutterkoerper
+ + Aktiv + + Inaktiv + + + + +
mit Jahresvarianten + +
+ + + + bis + + + RA
+ Dec +
+
+
- - - diff --git a/public/my_favorites.php b/public/my_favorites.php index 7bc469c..648a431 100644 --- a/public/my_favorites.php +++ b/public/my_favorites.php @@ -109,6 +109,20 @@ function setExclusiveFavorite(PDO $pdo, string $tableName, string $idColumn, int } } +function countUserRows(PDO $pdo, string $tableName, int $userId): int +{ + $stmtCount = $pdo->prepare(" + SELECT COUNT(*) + FROM `{$tableName}` + WHERE `user_id` = :user_id + "); + $stmtCount->execute([ + ':user_id' => $userId, + ]); + + return (int) $stmtCount->fetchColumn(); +} + if ($_SERVER['REQUEST_METHOD'] === 'POST') { $action = $_POST['action'] ?? ''; @@ -168,17 +182,89 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if (empty($errors)) { - $stmtDelete = $pdo->prepare(" - DELETE FROM `app_user_satellites` + $stmtSatellite = $pdo->prepare(" + SELECT `satellite_id`, `is_favorite` + FROM `app_user_satellites` WHERE `user_id` = :user_id AND `satellite_id` = :satellite_id + LIMIT 1 "); - $stmtDelete->execute([ + $stmtSatellite->execute([ ':user_id' => $userId, ':satellite_id' => $satelliteId, ]); + $satelliteRow = $stmtSatellite->fetch(); - $successMessage = 'Der Satellit wurde entfernt.'; + if (!$satelliteRow) { + $errors[] = 'Der Satellit ist nicht in deiner Liste vorhanden.'; + } + } + + if (empty($errors)) { + $satelliteCount = countUserRows($pdo, 'app_user_satellites', $userId); + if ($satelliteCount <= 1) { + $errors[] = 'Der letzte Satellit kann nicht geloescht werden.'; + } + } + + if (empty($errors)) { + $pdo->beginTransaction(); + + try { + $stmtDelete = $pdo->prepare(" + DELETE FROM `app_user_satellites` + WHERE `user_id` = :user_id + AND `satellite_id` = :satellite_id + "); + $stmtDelete->execute([ + ':user_id' => $userId, + ':satellite_id' => $satelliteId, + ]); + + if ((int) ($satelliteRow['is_favorite'] ?? 0) === 1) { + $stmtNextFavorite = $pdo->prepare(" + SELECT `satellite_id` + FROM `app_user_satellites` + WHERE `user_id` = :user_id + ORDER BY `created_at` ASC, `satellite_id` ASC + LIMIT 1 + "); + $stmtNextFavorite->execute([ + ':user_id' => $userId, + ]); + $nextSatelliteId = $stmtNextFavorite->fetchColumn(); + + if ($nextSatelliteId !== false) { + $stmtResetFavorites = $pdo->prepare(" + UPDATE `app_user_satellites` + SET `is_favorite` = 0 + WHERE `user_id` = :user_id + "); + $stmtResetFavorites->execute([ + ':user_id' => $userId, + ]); + + $stmtSetFavorite = $pdo->prepare(" + UPDATE `app_user_satellites` + SET `is_favorite` = 1 + WHERE `user_id` = :user_id + AND `satellite_id` = :satellite_id + "); + $stmtSetFavorite->execute([ + ':user_id' => $userId, + ':satellite_id' => (int) $nextSatelliteId, + ]); + } + } + + $pdo->commit(); + $successMessage = 'Der Satellit wurde entfernt.'; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $errors[] = 'Der Satellit konnte nicht entfernt werden.'; + } } } @@ -189,6 +275,32 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { $errors[] = 'Ungueltiger Satellit.'; } + if (empty($errors)) { + $stmtCurrentSatellite = $pdo->prepare(" + SELECT `is_favorite` + FROM `app_user_satellites` + WHERE `user_id` = :user_id + AND `satellite_id` = :satellite_id + LIMIT 1 + "); + $stmtCurrentSatellite->execute([ + ':user_id' => $userId, + ':satellite_id' => $satelliteId, + ]); + $currentSatelliteFavorite = $stmtCurrentSatellite->fetchColumn(); + + if ($currentSatelliteFavorite === false) { + $errors[] = 'Der Satellit ist nicht in deiner Liste vorhanden.'; + } + } + + if (empty($errors)) { + $satelliteCount = countUserRows($pdo, 'app_user_satellites', $userId); + if ($satelliteCount <= 1 && (int) $currentSatelliteFavorite === 1) { + $errors[] = 'Beim letzten Satelliten kann der Favorit nicht entfernt werden.'; + } + } + if (empty($errors)) { if (!setExclusiveFavorite($pdo, 'app_user_satellites', 'satellite_id', $userId, $satelliteId)) { $errors[] = 'Der Satellit ist nicht in deiner Liste vorhanden.'; @@ -450,17 +562,89 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { } if (empty($errors)) { - $stmtDelete = $pdo->prepare(" - DELETE FROM `app_user_locations` + $stmtLocation = $pdo->prepare(" + SELECT `id`, `is_default` + FROM `app_user_locations` WHERE `id` = :id AND `user_id` = :user_id + LIMIT 1 "); - $stmtDelete->execute([ + $stmtLocation->execute([ ':id' => $locationId, ':user_id' => $userId, ]); + $locationRow = $stmtLocation->fetch(); - $successMessage = 'Der Standort wurde geloescht.'; + if (!$locationRow) { + $errors[] = 'Der Standort wurde nicht gefunden.'; + } + } + + if (empty($errors)) { + $locationCount = countUserRows($pdo, 'app_user_locations', $userId); + if ($locationCount <= 1) { + $errors[] = 'Der letzte Standort kann nicht geloescht werden.'; + } + } + + if (empty($errors)) { + $pdo->beginTransaction(); + + try { + $stmtDelete = $pdo->prepare(" + DELETE FROM `app_user_locations` + WHERE `id` = :id + AND `user_id` = :user_id + "); + $stmtDelete->execute([ + ':id' => $locationId, + ':user_id' => $userId, + ]); + + if ((int) ($locationRow['is_default'] ?? 0) === 1) { + $stmtNextDefault = $pdo->prepare(" + SELECT `id` + FROM `app_user_locations` + WHERE `user_id` = :user_id + ORDER BY `name` ASC, `id` ASC + LIMIT 1 + "); + $stmtNextDefault->execute([ + ':user_id' => $userId, + ]); + $nextLocationId = $stmtNextDefault->fetchColumn(); + + if ($nextLocationId !== false) { + $stmtResetDefaults = $pdo->prepare(" + UPDATE `app_user_locations` + SET `is_default` = 0 + WHERE `user_id` = :user_id + "); + $stmtResetDefaults->execute([ + ':user_id' => $userId, + ]); + + $stmtSetDefault = $pdo->prepare(" + UPDATE `app_user_locations` + SET `is_default` = 1 + WHERE `user_id` = :user_id + AND `id` = :id + "); + $stmtSetDefault->execute([ + ':user_id' => $userId, + ':id' => (int) $nextLocationId, + ]); + } + } + + $pdo->commit(); + $successMessage = 'Der Standort wurde geloescht.'; + } catch (Throwable $e) { + if ($pdo->inTransaction()) { + $pdo->rollBack(); + } + $errors[] = 'Der Standort konnte nicht geloescht werden.'; + } } } } @@ -697,11 +881,15 @@ $loggedIn = true; -
- - - -
+ 1): ?> +
+ + + +
+ + Letzter Standort + @@ -893,18 +1081,26 @@ $loggedIn = true; -
- - - -
-
- - - -
+ + Favorit bleibt gesetzt + +
+ + + +
+ + 1): ?> +
+ + + +
+ + Letzter Satellit + diff --git a/public/py/__pycache__/api.cpython-312.pyc b/public/py/__pycache__/api.cpython-312.pyc index ea68f3d7a82b1309f7ca5353a6dd4f9084a3721d..3deeb87e8e4de95519e00532725e0296a148e49c 100644 GIT binary patch delta 26698 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 DsW@pj delta 23068 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 P>&Ja#Hd(r;CGr0NourOe diff --git a/public/py/api.py b/public/py/api.py index a60e11f..02d60fb 100644 --- a/public/py/api.py +++ b/public/py/api.py @@ -93,6 +93,117 @@ def serialize_event(label: str, event_time: astronomy.Time | None, tz: ZoneInfo) } +def normalize_longitude_deg(value: float) -> float: + value = math.fmod(value, 360.0) + if value < 0.0: + value += 360.0 + return value + + +def action_solar_longitude_to_datetime(args: list[str]) -> dict: + if len(args) != 3: + fail( + "Aktion solar_longitude_to_datetime erwartet 3 Argumente: lambda_target_deg year timezone", + extra={"argv": args}, + ) + + lambda_target_deg = parse_float(args[0], "Solare Laenge") + lambda_target_deg = normalize_longitude_deg(lambda_target_deg) + + try: + year = int(args[1]) + except ValueError as exc: + fail("Jahr ist ungueltig.", extra={"details": str(exc), "argv": args}) + + timezone_name = args[2] + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: # pragma: no cover + fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)}) + + start_dt_utc = datetime(year, 1, 1, 0, 0, 0, tzinfo=timezone.utc) + start_time = dt_to_time(start_dt_utc) + lambda0_deg = float(astronomy.SunPosition(start_time).elon) + delta_lambda_deg = normalize_longitude_deg(lambda_target_deg - lambda0_deg) + delta_t_days = delta_lambda_deg / 0.98564736 + + estimated_dt_utc = start_dt_utc + timedelta(days=delta_t_days) + search_start_dt_utc = estimated_dt_utc - timedelta(days=5) + search_start_time = dt_to_time(search_start_dt_utc) + result_time = astronomy.SearchSunLongitude(lambda_target_deg, search_start_time, 10.0) + + if result_time is None: + fail( + "Kein Zeitpunkt fuer die angegebene solare Laenge gefunden.", + extra={ + "lambda_target_deg": lambda_target_deg, + "year": year, + "timezone": timezone_name, + }, + ) + + utc_dt = time_to_datetime(result_time) + local_dt = utc_dt.astimezone(tz) + + return { + "ok": True, + "action": "solar_longitude_to_datetime", + "input": { + "lambda_target_deg": lambda_target_deg, + "year": year, + "timezone": timezone_name, + }, + "reference": { + "utc_iso": start_dt_utc.isoformat().replace("+00:00", "Z"), + "lambda0_deg": lambda0_deg, + }, + "estimate": { + "delta_lambda_deg": delta_lambda_deg, + "delta_t_days": delta_t_days, + "estimated_utc_iso": estimated_dt_utc.isoformat().replace("+00:00", "Z"), + }, + "result": { + "utc_iso": utc_dt.isoformat().replace("+00:00", "Z"), + "local_iso": local_dt.isoformat(), + "local_date": local_dt.strftime("%Y-%m-%d"), + "local_time": local_dt.strftime("%H:%M:%S"), + "jd_tt": float(result_time.tt) + 2451545.0, + "jd_ut": float(result_time.ut) + 2451545.0, + }, + } + + +def action_current_solar_longitude(args: list[str]) -> dict: + if len(args) != 1: + fail( + "Aktion current_solar_longitude erwartet 1 Argument: timezone", + extra={"argv": args}, + ) + + timezone_name = args[0] + try: + tz = ZoneInfo(timezone_name) + except Exception as exc: # pragma: no cover + fail("Zeitzone ist ungueltig oder auf dem Server nicht verfuegbar.", extra={"details": str(exc)}) + + utc_dt = datetime.now(timezone.utc) + time_value = dt_to_time(utc_dt) + solar_longitude_deg = float(astronomy.SunPosition(time_value).elon) + local_dt = utc_dt.astimezone(tz) + + return { + "ok": True, + "action": "current_solar_longitude", + "result": { + "solar_longitude_deg": solar_longitude_deg, + "utc_iso": utc_dt.isoformat().replace("+00:00", "Z"), + "local_iso": local_dt.isoformat(), + "local_date": local_dt.strftime("%Y-%m-%d"), + "local_time": local_dt.strftime("%H:%M:%S"), + }, + } + + def search_event( body: astronomy.Body, direction: astronomy.Direction, @@ -4014,7 +4125,7 @@ def action_satellite_passes(args: list[str]) -> dict: def main() -> None: if len(sys.argv) < 2: - fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]}) + fail("Es wurde keine Aktion uebergeben.", extra={"available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]}) action = sys.argv[1] args = sys.argv[2:] @@ -4024,6 +4135,16 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return + if action == "solar_longitude_to_datetime": + result = action_solar_longitude_to_datetime(args) + print(json.dumps(result, ensure_ascii=True)) + return + + if action == "current_solar_longitude": + result = action_current_solar_longitude(args) + print(json.dumps(result, ensure_ascii=True)) + return + if action == "moon_star_occultations": result = action_moon_star_occultations(args) print(json.dumps(result, ensure_ascii=True)) @@ -4149,7 +4270,7 @@ def main() -> None: print(json.dumps(result, ensure_ascii=True)) return - fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]}) + fail("Unbekannte Aktion.", extra={"action": action, "available_actions": ["sun_moon_rise_set", "solar_longitude_to_datetime", "current_solar_longitude", "moon_star_occultations", "moon_phase_details", "satellite_passes", "planet_rise_set", "month_sky_context", "moon_phases_for_month", "golden_handle_for_month", "season_changes_for_month", "time_changes_for_month", "moon_planet_approaches", "moon_deep_sky_approaches_for_month", "planet_bright_star_approaches_for_month", "planet_conjunctions_for_month", "eclipses_for_month", "moon_apsides_for_month", "sun_apsides_for_month", "inner_planet_elongations_for_month", "venus_peak_magnitude_for_month", "outer_planet_events_for_month", "outer_planet_stations_for_month", "jupiter_moons_one_side_for_month", "mercury_good_visibility_for_month", "planet_parades_for_month", "planet_constellation_changes_for_month", "moon_star_occultations_for_month"]}) if __name__ == "__main__":