generated from ztimson/template
Compare commits
4
Commits
b74d278c8e
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c965dcb30 | ||
|
|
e71c877214 | ||
|
|
fb5cd8fcdc | ||
|
|
92ef420d89 |
@@ -0,0 +1,3 @@
|
||||
.idea
|
||||
vscode
|
||||
data
|
||||
@@ -1,51 +0,0 @@
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 26294.04791053 .00009575 00000+0 18188-3 0 9996
|
||||
2 25544 51.6327 37.2623 0006759 289.5321 70.4938 15.48967427591263
|
||||
POISK
|
||||
1 36086U 09060A 26294.04791053 .00009575 00000+0 18188-3 0 9994
|
||||
2 36086 51.6327 37.2623 0006759 289.5321 70.4938 15.48967427586484
|
||||
CSS (TIANHE)
|
||||
1 48274U 21035A 26294.25846467 .00030091 00000+0 35038-3 0 9995
|
||||
2 48274 41.4687 276.1937 0002031 190.5094 169.5703 15.61026916312853
|
||||
ISS (NAUKA)
|
||||
1 49044U 21066A 26294.04791053 .00009575 00000+0 18188-3 0 9992
|
||||
2 49044 51.6327 37.2623 0006759 289.5321 70.4938 15.48967427586527
|
||||
FREGAT DEB
|
||||
1 49271U 11037PF 26292.86068782 .00010680 00000+0 14889-1 0 9996
|
||||
2 49271 51.6485 345.9436 0940020 202.8908 152.7302 12.45374907240561
|
||||
CSS (WENTIAN)
|
||||
1 53239U 22085A 26294.25846467 .00030091 00000+0 35038-3 0 9998
|
||||
2 53239 41.4687 276.1937 0002031 190.5094 169.5703 15.61026916288234
|
||||
CSS (MENGTIAN)
|
||||
1 54216U 22143A 26294.25846467 .00030091 00000+0 35038-3 0 9999
|
||||
2 54216 41.4687 276.1937 0002031 190.5094 169.5703 15.61026916308086
|
||||
SZ-21 MODULE
|
||||
1 66515U 25246C 26294.22802189 .00131005 13520-4 39861-3 0 9992
|
||||
2 66515 41.4681 234.9893 0012983 217.8317 142.1621 15.91235557 53715
|
||||
DUPLEX
|
||||
1 66906U 98067XT 26294.21739889 .00087112 00000+0 60248-3 0 9998
|
||||
2 66906 51.6239 8.2935 0005169 333.1895 26.8836 15.73765430 50420
|
||||
KNACKSAT-2
|
||||
1 67683U 98067YA 26294.23849680 .00126976 00000+0 84569-3 0 9998
|
||||
2 67683 51.6243 14.4676 0009852 333.8147 26.2353 15.74432101 40030
|
||||
GXIBA-1
|
||||
1 67685U 98067YC 26294.23381720 .00198887 00000+0 98256-3 0 9994
|
||||
2 67685 51.6190 10.6184 0011279 337.3691 22.6813 15.80874177 40080
|
||||
UITMSAT-2
|
||||
1 67686U 98067YD 26293.27790080 .03059098 11989-4 11168-2 0 9993
|
||||
2 67686 51.6061 4.7855 0010166 350.7558 9.3273 16.23807297 40070
|
||||
LEOPARD
|
||||
1 67687U 98067YE 26294.22233459 .00713598 22293-3 18935-2 0 9999
|
||||
2 67687 51.6147 9.8036 0013002 350.0326 10.0421 15.93899698 40011
|
||||
HMU-SAT2
|
||||
1 67688U 98067YF 26294.20241447 .00414307 77386-4 11821-2 0 9991
|
||||
2 67688 51.6195 5.8753 0010290 344.6935 15.3759 15.92614788 40069
|
||||
PROGRESS-MS 34
|
||||
1 68837U 26093A 26294.04791053 .00009575 00000+0 18188-3 0 9998
|
||||
2 68837 51.6327 37.2623 0006759 289.5321 70.4938 15.48967427587451
|
||||
TIANZHOU-10
|
||||
1 69049U 26102A 26294.25846467 .00030091 00000+0 35038-3 0 9998
|
||||
2 69049 41.4687 276.1937 0002031 190.5094 169.5703 15.61026916311024
|
||||
SHENZHOU-23 (SZ-23)
|
||||
1 69180U 26113A 26294.25846467 .00030091 00000+0 35038-3 0 9996
|
||||
2 69180 41.4687 276.1937 0002031 190.5094 169.5703 15.61026916307902
|
||||
@@ -1,465 +0,0 @@
|
||||
ATLAS CENTAUR 2
|
||||
1 00694U 63047A 26294.56101685 .00002576 00000+0 29898-3 0 9992
|
||||
2 00694 30.3591 12.5187 0544654 272.2554 81.5810 14.12859964163586
|
||||
THOR AGENA D R/B
|
||||
1 00733U 64002A 26294.30136440 .00000121 00000+0 60327-4 0 9994
|
||||
2 00733 99.1093 319.8069 0032160 322.8889 37.0070 14.34104978270630
|
||||
SL-3 R/B
|
||||
1 00877U 64053B 26294.51523957 .00000234 00000+0 69680-4 0 9991
|
||||
2 00877 65.0788 29.0938 0074004 220.6204 138.9362 14.61481505299793
|
||||
SL-8 R/B
|
||||
1 02802U 67045B 26294.23409436 .00000170 00000+0 54709-4 0 9994
|
||||
2 02802 74.0094 286.8215 0065348 240.4721 118.9912 14.45730904121615
|
||||
SL-8 R/B
|
||||
1 03230U 68040B 26294.49258764 .00002378 00000+0 17237-3 0 9996
|
||||
2 03230 74.0307 211.8620 0024606 327.5937 32.3739 15.04368628123955
|
||||
OAO 2
|
||||
1 03597U 68110A 26294.50312536 .00000165 00000+0 49508-4 0 9999
|
||||
2 03597 34.9927 84.3502 0005835 215.5980 144.4322 14.47391459 51837
|
||||
ISIS 1
|
||||
1 03669U 69009A 26294.49194491 .00000102 00000+0 68259-4 0 9998
|
||||
2 03669 88.4316 130.5092 1707004 353.3017 4.7579 11.30455333374045
|
||||
SERT 2
|
||||
1 04327U 70009A 26294.33933006 .00000000 00000+0 56942-4 0 9991
|
||||
2 04327 99.2922 225.3083 0004358 318.8185 199.9837 13.58748563810531
|
||||
SL-3 R/B
|
||||
1 05118U 71028B 26294.16090038 .00137395 00000+0 54759-3 0 9993
|
||||
2 05118 81.2224 129.0235 0007329 180.7902 179.3355 15.86191662 24099
|
||||
ASTEX 1
|
||||
1 05560U 71089A 26294.21419375 .00000488 00000+0 12372-3 0 9993
|
||||
2 05560 92.7166 224.5323 0017274 48.2220 312.0459 14.50935156895004
|
||||
SL-8 R/B
|
||||
1 05730U 71119B 26294.24000555 .00006899 00000+0 35210-3 0 9993
|
||||
2 05730 73.8834 112.9882 0536964 32.8816 330.4563 14.40856893687948
|
||||
OAO 3 (COPERNICUS)
|
||||
1 06153U 72065A 26294.14876516 .00000280 00000+0 61614-4 0 9997
|
||||
2 06153 35.0037 92.6434 0006899 211.4386 316.3107 14.60118887879166
|
||||
ATLAS CENTAUR R/B
|
||||
1 06155U 72065B 26294.05892625 .00001236 00000+0 17001-3 0 9990
|
||||
2 06155 35.0009 64.5075 0032636 108.0164 252.4097 14.78100388874629
|
||||
SL-8 R/B
|
||||
1 08459U 75112B 26294.07641126 .00000130 00000+0 51207-4 0 9994
|
||||
2 08459 74.0606 324.2692 0015795 353.8293 6.2664 14.38290401664270
|
||||
SL-3 R/B
|
||||
1 10114U 77057B 26294.24700653 .00015195 00000+0 26969-3 0 9998
|
||||
2 10114 97.5866 117.1779 0006444 176.2330 183.8971 15.50301719703226
|
||||
SEASAT 1
|
||||
1 10967U 78064A 26294.26126382 .00000331 00000+0 13062-3 0 9992
|
||||
2 10967 108.0112 238.9084 0002626 282.9298 77.1555 14.46311937539127
|
||||
SL-14 R/B
|
||||
1 11267U 79011B 26294.12007023 .00000589 00000+0 55282-4 0 9990
|
||||
2 11267 82.5235 242.6317 0016732 29.4145 330.8013 14.92105915562393
|
||||
SL-8 R/B
|
||||
1 11574U 79089B 26294.29366548 .00000206 00000+0 70126-4 0 9994
|
||||
2 11574 74.0710 33.4017 0015020 151.2736 208.9251 14.42449487468631
|
||||
SL-14 R/B
|
||||
1 11672U 80005B 26294.01336570 .00000561 00000+0 53889-4 0 9996
|
||||
2 11672 82.5114 93.4751 0016834 128.4682 231.8050 14.91120181510061
|
||||
SL-8 R/B
|
||||
1 12139U 81003B 26294.12759279 .00028338 00000+0 63788-3 0 9991
|
||||
2 12139 82.9328 80.3648 0237016 109.5922 253.1068 15.18683164348807
|
||||
SL-3 R/B
|
||||
1 12465U 81046B 26294.18985731 .00013767 00000+0 27420-3 0 9993
|
||||
2 12465 81.2324 17.9798 0016053 312.4179 47.5707 15.46625166473234
|
||||
SL-3 R/B
|
||||
1 12904U 81103B 26294.22228109 .00003915 00000+0 16463-3 0 9998
|
||||
2 12904 81.1567 308.4582 0011490 241.5519 118.4556 15.23121464 7554
|
||||
SL-3 R/B
|
||||
1 13068U 82013B 26294.22371036 .00004221 00000+0 17459-3 0 9996
|
||||
2 13068 81.1718 256.0324 0022670 134.8965 225.4114 15.23496062438676
|
||||
SL-3 R/B
|
||||
1 13154U 82039B 26294.33527760 .00002769 00000+0 16251-3 0 9993
|
||||
2 13154 81.1507 315.3258 0029958 294.6783 65.1324 15.11054370415702
|
||||
SL-3 R/B
|
||||
1 13403U 82079B 26294.25313995 .00005384 00000+0 20209-3 0 9992
|
||||
2 13403 81.1430 286.2552 0024853 119.2619 241.1104 15.26723012412099
|
||||
SL-14 R/B
|
||||
1 13553U 82092B 26293.96636237 .00000417 00000+0 43966-4 0 9995
|
||||
2 13553 82.5646 290.6041 0018673 274.0970 115.8758 14.86821133362403
|
||||
SL-3 R/B
|
||||
1 13819U 83010B 26294.14380510 .00018335 00000+0 29989-3 0 9998
|
||||
2 13819 81.0983 84.4792 0013790 336.7509 23.3113 15.52273997392850
|
||||
SL-3 R/B
|
||||
1 14208U 83075B 26294.32089952 .00011556 00000+0 26536-3 0 9991
|
||||
2 14208 97.6104 308.1892 0020927 135.7527 224.5398 15.42639408364890
|
||||
COSMOS 1536
|
||||
1 14699U 84013A 26294.28606415 .00007210 00000+0 27624-3 0 9993
|
||||
2 14699 82.5427 243.1282 0012735 199.6447 160.4302 15.26279903307009
|
||||
SL-14 R/B
|
||||
1 14820U 84027B 26294.26054980 .00000377 00000+0 40162-4 0 9996
|
||||
2 14820 82.5442 33.4590 0016676 309.5364 50.4378 14.86131564281133
|
||||
SL-8 R/B
|
||||
1 15483U 85006B 26294.24643130 .00000181 00000+0 68533-4 0 9994
|
||||
2 15483 74.0462 206.0772 0017260 63.5131 100.8516 14.37878570186449
|
||||
SL-12 R/B(2)
|
||||
1 15772U 85042D 26294.14134787 .00000108 00000+0 69934-4 0 9992
|
||||
2 15772 71.1087 331.4022 0033429 137.5471 247.6980 14.22027261146444
|
||||
SL-14 R/B
|
||||
1 15945U 85069B 26294.17608976 .00000319 00000+0 33148-4 0 9994
|
||||
2 15945 82.5285 172.8823 0019137 160.8339 199.3602 14.86589607206336
|
||||
SL-16 R/B
|
||||
1 16182U 85097B 26294.23232751 .00000230 00000+0 13935-3 0 9993
|
||||
2 16182 71.0051 25.7110 0009780 272.2527 114.8353 14.16622922119170
|
||||
SL-14 R/B
|
||||
1 16496U 86006B 26294.29039982 .00000656 00000+0 66835-4 0 9999
|
||||
2 16496 82.5283 47.4159 0074844 244.0734 115.2757 14.87041078182736
|
||||
COSMOS 1743
|
||||
1 16719U 86034A 26294.12877806 .00127340 00000+0 61222-3 0 9999
|
||||
2 16719 82.5235 193.4905 0005564 116.8516 243.3322 15.82275405215590
|
||||
SL-14 R/B
|
||||
1 16792U 86046B 26294.23125326 .00000406 00000+0 43766-4 0 9995
|
||||
2 16792 82.4828 227.0671 0022400 198.6083 161.4317 14.85745170159894
|
||||
SL-14 R/B
|
||||
1 16882U 86055B 26293.42945654 .00000456 00000+0 49590-4 0 9995
|
||||
2 16882 82.5173 275.3776 0017629 311.9347 207.3823 14.85795696152736
|
||||
AJISAI (EGS)
|
||||
1 16908U 86061A 26294.32308455 -.00000069 00000+0 20409-3 0 9991
|
||||
2 16908 50.0065 119.5286 0011424 140.6282 43.4000 12.44517827496099
|
||||
SL-14 R/B
|
||||
1 17567U 87024B 26294.24999466 .00000418 00000+0 44613-4 0 9993
|
||||
2 17567 82.5278 240.7518 0016885 230.5706 129.4017 14.86287319121634
|
||||
COSMOS 1833
|
||||
1 17589U 87027A 26294.18432040 -.00000007 00000+0 21778-4 0 9995
|
||||
2 17589 70.9156 212.0275 0025379 226.4965 133.4047 14.13038349 42171
|
||||
SL-16 R/B
|
||||
1 17590U 87027B 26294.06704182 -.00000205 00000+0 -77812-4 0 9994
|
||||
2 17590 71.0020 161.5628 0003843 164.1692 195.9553 14.17187883 47275
|
||||
SL-14 R/B
|
||||
1 17912U 87038B 26294.15931650 .00000454 00000+0 49629-4 0 9995
|
||||
2 17912 82.4989 84.4015 0020235 136.1059 224.1769 14.85516185112707
|
||||
COSMOS 1844
|
||||
1 17973U 87041A 26294.25958998 -.00000003 00000+0 23675-4 0 9995
|
||||
2 17973 70.8985 278.3742 0029259 39.6902 320.6354 14.14594795 35487
|
||||
SL-14 R/B
|
||||
1 18153U 87055B 26294.29841810 .00000332 00000+0 35335-4 0 9999
|
||||
2 18153 82.4973 136.3561 0020356 351.7670 8.3209 14.85777631103435
|
||||
COSMOS 1867
|
||||
1 18187U 87060A 26294.30155787 -.00000060 00000+0 15261-4 0 9990
|
||||
2 18187 65.0125 34.8631 0019228 279.6183 80.2729 14.31527061 52239
|
||||
SL-14 R/B
|
||||
1 18749U 88001B 26294.10692514 .00000315 00000+0 33516-4 0 9997
|
||||
2 18749 82.5145 343.2937 0017242 208.4591 151.5686 14.85678766 75497
|
||||
COSMOS 1933
|
||||
1 18958U 88020A 26294.24791172 .00003488 00000+0 16324-3 0 9996
|
||||
2 18958 82.5153 27.5089 0005225 124.0604 236.1127 15.19421982 84465
|
||||
SL-3 R/B
|
||||
1 19046U 88032B 26294.27064488 .00004404 00000+0 19605-3 0 9998
|
||||
2 19046 97.5406 277.3653 0025511 162.5809 197.6308 15.21711122 86394
|
||||
SL-16 R/B
|
||||
1 19120U 88039B 26294.03608271 .00000043 00000+0 42968-4 0 9990
|
||||
2 19120 71.0142 312.0473 0022249 348.0279 12.0314 14.19640800990363
|
||||
COSMOS 1953
|
||||
1 19210U 88050A 26294.05193265 .00008197 00000+0 26793-3 0 9992
|
||||
2 19210 82.5042 290.9622 0011841 54.3397 305.8945 15.31491381 72247
|
||||
SL-8 R/B
|
||||
1 19257U 88053B 26293.77898947 .00000179 00000+0 66363-4 0 9993
|
||||
2 19257 74.0480 57.5380 0020083 185.4423 283.7756 14.39010557 8862
|
||||
COSMOS 1975
|
||||
1 19573U 88093A 26293.91870757 .00008212 00000+0 27462-3 0 9995
|
||||
2 19573 82.5166 251.0905 0013155 93.5987 266.6759 15.30741044 53832
|
||||
SL-14 R/B
|
||||
1 19574U 88093B 26294.29279010 .00000381 00000+0 40800-4 0 9994
|
||||
2 19574 82.5338 190.2547 0021061 15.6501 124.1693 14.85811637 34516
|
||||
SL-16 R/B
|
||||
1 19650U 88102B 26294.29893222 -.00000161 00000+0 -57215-4 0 9993
|
||||
2 19650 71.0018 36.7810 0013447 17.6045 342.5542 14.16178709958839
|
||||
INTERCOSMOS 24
|
||||
1 20261U 89080A 26294.30039529 .00000382 00000+0 10330-3 0 9992
|
||||
2 20261 82.6042 317.5605 1187932 129.1241 242.1687 12.59762048695209
|
||||
SL-14 R/B
|
||||
1 20262U 89080C 26294.25005201 .00000138 00000+0 34618-4 0 9992
|
||||
2 20262 82.5970 28.3168 1230200 29.1921 337.2544 12.50614866688341
|
||||
DELTA 1 R/B
|
||||
1 20323U 89089B 26294.22933088 .00000361 00000+0 97512-4 0 9999
|
||||
2 20323 97.0669 105.6621 0070040 128.8032 231.9449 14.50798015928433
|
||||
ARIANE 40 R/B
|
||||
1 20443U 90005H 26294.09424480 .00000173 00000+0 68480-4 0 9991
|
||||
2 20443 98.2810 347.7000 0009071 242.7083 117.3183 14.39976157925245
|
||||
COSMOS 2058
|
||||
1 20465U 90010A 26294.24712406 .00003861 00000+0 21928-3 0 9998
|
||||
2 20465 82.4696 184.9850 0007630 279.7266 80.3103 15.12632758976326
|
||||
SL-14 R/B
|
||||
1 20466U 90010B 26294.28368385 .00000411 00000+0 46747-4 0 9991
|
||||
2 20466 82.5028 55.4969 0018228 276.7781 83.1359 14.83711824961938
|
||||
SL-14 R/B
|
||||
1 20511U 90018B 26294.18125476 .00000316 00000+0 36482-4 0 9990
|
||||
2 20511 82.5245 12.3158 0017472 72.1176 288.1943 14.82283562955984
|
||||
HST
|
||||
1 20580U 90037B 26294.30934801 .00007548 00000+0 22957-3 0 9996
|
||||
2 20580 28.4737 297.7404 0002158 117.4266 242.6550 15.32029197807923
|
||||
SL-16 R/B
|
||||
1 20625U 90046B 26294.13145192 -.00000252 00000+0 -10531-3 0 9996
|
||||
2 20625 70.9995 184.4335 0015971 315.0159 44.9669 14.15038641880288
|
||||
COSMOS 2084
|
||||
1 20663U 90055A 26294.25792321 .00002715 00000+0 19538-3 0 9993
|
||||
2 20663 62.7890 285.6424 0023108 356.7392 3.3554 15.06556923970290
|
||||
SL-6 R/B(2)
|
||||
1 20666U 90055D 26294.15909047 .00002369 00000+0 17694-3 0 9996
|
||||
2 20666 62.7822 206.5630 0060102 20.9119 339.4419 15.04407183972107
|
||||
SL-8 R/B
|
||||
1 20775U 90078B 26294.29034957 .00013826 00000+0 38055-3 0 9990
|
||||
2 20775 82.9019 47.3274 0335937 330.6802 27.5923 14.95752071842958
|
||||
SL-8 R/B
|
||||
1 21088U 91006B 26294.15169935 .00000037 00000+0 21498-4 0 9994
|
||||
2 21088 82.9461 2.3099 0023689 121.9838 265.6896 13.77526194795114
|
||||
OKEAN-3
|
||||
1 21397U 91039A 26293.95126074 .00001979 00000+0 13447-3 0 9997
|
||||
2 21397 82.5137 118.3940 0012733 282.6573 77.3230 15.05796323898924
|
||||
COSMOS 2151
|
||||
1 21422U 91042A 26294.22290472 .00001900 00000+0 14761-3 0 9999
|
||||
2 21422 82.4975 226.0200 0012971 23.9043 336.2780 15.00730995897014
|
||||
SL-14 R/B
|
||||
1 21423U 91042B 26294.21464806 .00000304 00000+0 35395-4 0 9997
|
||||
2 21423 82.4961 8.6868 0016083 215.7171 144.2969 14.81964135886472
|
||||
ERS-1
|
||||
1 21574U 91050A 26294.31804735 .00000271 00000+0 10125-3 0 9999
|
||||
2 21574 98.7738 318.0697 0030874 252.0137 107.7684 14.39583492848190
|
||||
ARIANE 40 R/B
|
||||
1 21610U 91050F 26294.11125504 .00000291 00000+0 99135-4 0 9999
|
||||
2 21610 98.1733 337.5353 0004338 106.0238 254.1431 14.43018386851507
|
||||
INTERCOSMOS 25
|
||||
1 21819U 91086A 26294.13656242 .00002184 00000+0 42577-3 0 9999
|
||||
2 21819 82.6063 173.6394 1484901 350.7474 6.8764 12.12444658524007
|
||||
SL-8 R/B
|
||||
1 21876U 92008B 26293.93489035 -.00000005 00000+0 -20511-4 0 9995
|
||||
2 21876 82.9231 100.2884 0033834 224.1070 247.6718 13.75991264740396
|
||||
SL-8 R/B
|
||||
1 21938U 92020B 26293.91496560 .00000017 00000+0 19700-5 0 9997
|
||||
2 21938 82.9329 346.4668 0029672 31.6535 141.2048 13.75562168732099
|
||||
USA 81
|
||||
1 21949U 92023A 26294.15460056 .00000136 00000+0 42977-4 0 9993
|
||||
2 21949 85.0115 1.3018 0002501 76.8614 283.2858 14.32425447127068
|
||||
COSMOS 2219
|
||||
1 22219U 92076A 26294.17347810 .00000319 00000+0 19174-3 0 9991
|
||||
2 22219 71.0594 266.6899 0019416 220.7513 139.2158 14.14117006750254
|
||||
SL-16 R/B
|
||||
1 22220U 92076B 26294.32267781 -.00000198 00000+0 -74873-4 0 9993
|
||||
2 22220 70.9996 73.0686 0010605 115.1482 263.3207 14.16885871753797
|
||||
COSMOS 2221
|
||||
1 22236U 92080A 26294.29652594 .00001841 00000+0 15810-3 0 9990
|
||||
2 22236 82.5031 62.8966 0014875 149.1191 211.0910 14.96836038814537
|
||||
SL-16 R/B
|
||||
1 22285U 92093B 26294.17478562 -.00000182 00000+0 -68634-4 0 9998
|
||||
2 22285 71.0212 250.8959 0006209 304.8474 55.2065 14.15478174745066
|
||||
COSMOS 2228
|
||||
1 22286U 92094A 26294.28821973 .00001721 00000+0 14350-3 0 9993
|
||||
2 22286 82.5209 21.2028 0016171 260.9185 99.0208 14.97929501809822
|
||||
SL-16 R/B
|
||||
1 22566U 93016B 26294.16827520 .00000275 00000+0 16554-3 0 9991
|
||||
2 22566 71.0048 101.6040 0011256 329.3389 30.7075 14.15324108734079
|
||||
COSMOS 2242
|
||||
1 22626U 93024A 26294.30616331 .00001708 00000+0 14512-3 0 9993
|
||||
2 22626 82.5194 50.8305 0014614 265.3740 94.5813 14.97220590814140
|
||||
SL-16 R/B
|
||||
1 22803U 93059B 26294.19291922 -.00000090 00000+0 -20553-4 0 9998
|
||||
2 22803 70.9895 255.5675 0018947 231.7896 128.1523 14.17262744711486
|
||||
ARIANE 40 R/B
|
||||
1 22830U 93061H 26294.17370659 .00000143 00000+0 68098-4 0 9997
|
||||
2 22830 98.4684 358.3255 0010193 342.3114 17.7715 14.32947975726395
|
||||
COSMOS 2278
|
||||
1 23087U 94023A 26293.77927596 -.00000038 00000+0 48023-5 0 9995
|
||||
2 23087 71.0526 64.1501 0007439 32.5573 327.6007 14.14406436676634
|
||||
SL-16 R/B
|
||||
1 23088U 94023B 26294.07855335 -.00000145 00000+0 -50046-4 0 9998
|
||||
2 23088 71.0004 317.5447 0005505 296.7581 63.2978 14.15037295677977
|
||||
SL-16 R/B
|
||||
1 23343U 94074B 26294.13746782 .00000349 00000+0 53193-4 0 9990
|
||||
2 23343 97.6921 315.5709 0006074 194.2103 206.4809 14.79588635720585
|
||||
SL-16 R/B
|
||||
1 23405U 94077B 26294.25505914 .00000068 00000+0 59009-4 0 9995
|
||||
2 23405 70.9818 237.9267 0003564 222.2211 137.8638 14.15581383648154
|
||||
ARIANE 40+ R/B
|
||||
1 23561U 95021B 26294.24599633 .00000242 00000+0 90797-4 0 9991
|
||||
2 23561 98.4287 2.4190 0006845 176.3773 221.0466 14.39889499651704
|
||||
SL-16 R/B
|
||||
1 23705U 95058B 26294.13154227 .00000220 00000+0 13650-3 0 9993
|
||||
2 23705 71.0210 66.9306 0011156 143.7619 216.4262 14.15517375599752
|
||||
SL-16 R/B
|
||||
1 24298U 96051B 26294.30076034 .00000131 00000+0 97343-4 0 9996
|
||||
2 24298 70.7716 57.7875 0016646 174.9726 185.1564 14.12491644553507
|
||||
ORBVIEW 2 (SEASTAR)
|
||||
1 24883U 97037A 26293.47621168 .00000203 00000+0 86721-4 0 9990
|
||||
2 24883 98.8796 104.0721 0003185 109.9485 250.2043 14.35604599540722
|
||||
SL-16 R/B
|
||||
1 25400U 98043G 26294.30376364 .00000069 00000+0 47922-4 0 9992
|
||||
2 25400 98.8541 249.6414 0010905 81.3944 278.8470 14.26086199471141
|
||||
SL-16 R/B
|
||||
1 25407U 98045B 26294.31428760 -.00000108 00000+0 -30535-4 0 9998
|
||||
2 25407 71.0114 67.0779 0006286 23.8158 336.3255 14.16217596459040
|
||||
ISS (ZARYA)
|
||||
1 25544U 98067A 26294.04791053 .00009575 00000+0 18188-3 0 9996
|
||||
2 25544 51.6327 37.2623 0006759 289.5321 70.4938 15.48967427591263
|
||||
CZ-4B R/B
|
||||
1 25732U 99025C 26294.33719284 .00000025 00000+0 32507-4 0 9991
|
||||
2 25732 98.6350 332.9185 0037991 56.6196 315.2278 14.18299802420159
|
||||
OKEAN-O
|
||||
1 25860U 99039A 26294.12249620 .00000534 00000+0 76347-4 0 9992
|
||||
2 25860 97.9164 351.3772 0001128 69.3361 290.7972 14.80585599467745
|
||||
SL-16 R/B
|
||||
1 25861U 99039B 26294.16291072 .00000428 00000+0 60734-4 0 9990
|
||||
2 25861 97.9686 352.5668 0013524 242.9352 117.0481 14.81946268470255
|
||||
DELTA 2 R/B
|
||||
1 25876U 99041E 26294.21996411 .00000244 00000+0 77391-4 0 9997
|
||||
2 25876 51.7636 120.1286 0499789 294.2377 60.7111 14.12282111394890
|
||||
HELIOS 1B
|
||||
1 25977U 99064A 26293.98227581 .00001297 00000+0 14671-3 0 9992
|
||||
2 25977 98.1512 202.6979 0002371 69.2953 290.8517 14.88275821718767
|
||||
TERRA
|
||||
1 25994U 99068A 26294.29679579 .00000306 00000+0 71201-4 0 9991
|
||||
2 25994 97.9307 338.7498 0002445 159.0733 268.7650 14.61181212428028
|
||||
SL-16 R/B
|
||||
1 26070U 00006B 26294.17611755 .00000287 00000+0 16980-3 0 9990
|
||||
2 26070 71.0040 343.7103 0017395 205.4030 154.6240 14.15848864380472
|
||||
TITAN 4B R/B
|
||||
1 26474U 00047B 26294.27087755 .00002861 00000+0 17734-3 0 9999
|
||||
2 26474 67.9867 125.5144 0038393 154.0237 206.2851 15.10759282426970
|
||||
ENVISAT
|
||||
1 27386U 02009A 26294.32593939 .00000102 00000+0 47067-4 0 9993
|
||||
2 27386 98.3992 242.9149 0001382 86.2548 79.9859 14.39089594291625
|
||||
IDEFIX & ARIANE 42P R/B
|
||||
1 27422U 02021B 26293.83843639 .00000205 00000+0 94201-4 0 9994
|
||||
2 27422 98.6037 230.0004 0010699 243.3867 116.6221 14.31130535276223
|
||||
AQUA
|
||||
1 27424U 02022A 26294.24047668 .00000750 00000+0 15869-3 0 9998
|
||||
2 27424 98.4457 266.1542 0000789 81.3616 335.0903 14.62246479301690
|
||||
CZ-4B R/B
|
||||
1 27432U 02024C 26294.13671150 .00000020 00000+0 32977-4 0 9996
|
||||
2 27432 98.8547 346.0449 0051127 102.2464 283.3267 14.14579456261114
|
||||
MIDORI II (ADEOS-II)
|
||||
1 27597U 02056A 26294.13845917 .00000242 00000+0 11288-3 0 9994
|
||||
2 27597 98.8059 254.5703 0002095 72.0178 288.1231 14.29404617242504
|
||||
H-2A R/B
|
||||
1 27601U 02056E 26294.16899731 .00000215 00000+0 89983-4 0 9994
|
||||
2 27601 98.3487 246.3466 0069360 209.1838 161.8114 14.33878371246428
|
||||
CZ-4B R/B
|
||||
1 28059U 03049C 26294.27226115 .00000305 00000+0 81421-4 0 9996
|
||||
2 28059 98.6961 298.1879 0046924 263.2025 96.3830 14.55172956219404
|
||||
SL-16 R/B
|
||||
1 28353U 04021B 26293.95726368 -.00000220 00000+0 -89411-4 0 9998
|
||||
2 28353 70.9989 119.7419 0003435 30.4791 329.6531 14.14625344154694
|
||||
CZ-4B R/B
|
||||
1 28415U 04035C 26294.04505285 .00008329 00000+0 26717-3 0 9991
|
||||
2 28415 97.3265 325.7652 0027986 244.7460 115.0881 15.32201368214386
|
||||
CZ-2C R/B
|
||||
1 28480U 04046B 26294.18880881 .00000077 00000+0 43779-4 0 9998
|
||||
2 28480 98.1821 195.0065 0138623 207.4318 151.9498 14.27060009140660
|
||||
ARIANE 5 R/B
|
||||
1 28499U 04049H 26294.22278216 .00000555 00000+0 67469-4 0 9992
|
||||
2 28499 98.2789 111.4444 0083249 340.9887 18.8224 14.85595290179270
|
||||
CZ-2D R/B
|
||||
1 28738U 05024B 26294.04569555 .00170858 00000+0 58992-3 0 9995
|
||||
2 28738 97.3349 321.2776 0002064 160.6930 199.4421 15.89107542175184
|
||||
ALOS (DAICHI)
|
||||
1 28931U 06002A 26294.13821038 .00001169 00000+0 20193-3 0 9994
|
||||
2 28931 98.1493 249.4287 0000923 52.5954 307.5336 14.70290243106601
|
||||
H-2A R/B
|
||||
1 28932U 06002B 26294.01776783 .00009893 00000+0 45362-3 0 9995
|
||||
2 28932 98.2688 101.5112 0048730 191.9517 168.0564 15.19614932128623
|
||||
RESURS-DK 1
|
||||
1 29228U 06021A 26293.94460919 .00002095 00000+0 12234-3 0 9996
|
||||
2 29228 69.9344 13.9115 0001857 145.8949 214.2337 15.13634042121602
|
||||
CZ-4B R/B
|
||||
1 29507U 06046C 26294.07270437 .00004527 00000+0 21936-3 0 9993
|
||||
2 29507 97.5316 338.2621 0031393 202.0720 157.9165 15.18622980 94237
|
||||
CZ-2C R/B
|
||||
1 31114U 07010B 26294.23275336 .00000052 00000+0 42845-4 0 9995
|
||||
2 31114 98.4169 36.4615 0058876 359.7408 0.3729 14.20300750 11728
|
||||
COSMO-SKYMED 1
|
||||
1 31598U 07023A 26294.30274207 .00002593 00000+0 22714-3 0 9993
|
||||
2 31598 97.8808 127.2592 0001782 83.9370 276.2055 14.97581828 48668
|
||||
COSMOS 2428
|
||||
1 31792U 07029A 26293.87466733 -.00000205 00000+0 -84202-4 0 9994
|
||||
2 31792 70.9406 90.6209 0012682 282.9421 77.0284 14.12972285995904
|
||||
SL-16 R/B
|
||||
1 31793U 07029B 26294.30042625 -.00000005 00000+0 22289-4 0 9998
|
||||
2 31793 70.9768 58.1179 0002429 283.3242 99.3377 14.14870320997287
|
||||
KORONAS-FOTON
|
||||
1 33504U 09003A 26294.01701681 .00015829 00000+0 29765-3 0 9990
|
||||
2 33504 82.4302 40.7173 0010519 63.5967 296.6361 15.48363595980559
|
||||
CZ-2C R/B
|
||||
1 37731U 11030B 26293.86025463 .00001321 00000+0 17083-3 0 9990
|
||||
2 37731 97.8826 247.0952 0051591 339.0207 74.4718 14.81693047823431
|
||||
H-2A R/B
|
||||
1 38341U 12025E 26294.03922116 .00002916 00000+0 26039-3 0 9996
|
||||
2 38341 98.4977 279.7084 0042104 295.7561 63.9315 14.96198069781903
|
||||
SHIJIAN-16 (SJ-16)
|
||||
1 39358U 13057A 26294.30712525 .00000308 00000+0 32557-4 0 9998
|
||||
2 39358 74.9707 117.7740 0016729 282.8340 77.0979 14.92835903705165
|
||||
SL-4 R/B
|
||||
1 39679U 14021B 26294.33679588 .00015465 00000+0 29158-3 0 9992
|
||||
2 39679 51.6045 311.5576 0084330 208.8775 150.7531 15.44832468691092
|
||||
ALOS-2
|
||||
1 39766U 14029A 26294.27444758 .00001011 00000+0 14222-3 0 9997
|
||||
2 39766 97.9227 29.6317 0001439 95.8521 264.2855 14.79469593670220
|
||||
YAOGAN-29
|
||||
1 41038U 15069A 26294.33955167 .00000157 00000+0 25824-4 0 9996
|
||||
2 41038 97.9952 323.1207 0001668 29.8734 330.2575 14.83165418589324
|
||||
ASTRO-H (HITOMI)
|
||||
1 41337U 16012A 26294.16089214 .00002314 00000+0 12951-3 0 9992
|
||||
2 41337 30.9998 340.2923 0009420 82.1891 277.9816 15.10999113586351
|
||||
HXMT (HUIYAN)
|
||||
1 42758U 17034A 26294.19381274 .00005174 00000+0 24781-3 0 9990
|
||||
2 42758 43.0134 200.9015 0032345 227.0757 132.7376 15.19415588516825
|
||||
SAOCOM 1A
|
||||
1 43641U 18076A 26293.70563936 .00000253 00000+0 38360-4 0 9994
|
||||
2 43641 97.8882 118.6571 0001540 83.6506 276.4883 14.82166739434690
|
||||
H-2A R/B
|
||||
1 43682U 18084L 26294.22095835 .00035644 00000+0 63059-3 0 9998
|
||||
2 43682 98.8391 201.0204 0018723 318.5042 41.4782 15.50048384440615
|
||||
SAOCOM 1B
|
||||
1 46265U 20059A 26294.27701272 .00000503 00000+0 69729-4 0 9994
|
||||
2 46265 97.8875 118.3204 0001401 86.6576 273.4798 14.82150573332163
|
||||
CSS (TIANHE)
|
||||
1 48274U 21035A 26294.25846467 .00030091 00000+0 35038-3 0 9995
|
||||
2 48274 41.4687 276.1937 0002031 190.5094 169.5703 15.61026916312853
|
||||
COSMOS 2550
|
||||
1 48865U 21056A 26294.26593659 .00005766 00000+0 18089-3 0 9995
|
||||
2 48865 67.1420 78.5160 0008356 269.5446 90.4751 15.33842010298055
|
||||
CZ-2C R/B
|
||||
1 52794U 22058K 26294.21535187 .00082714 00000+0 59237-3 0 9999
|
||||
2 52794 50.1805 223.0674 0026929 88.9328 271.4735 15.72446712245401
|
||||
CZ-2C R/B
|
||||
1 54039U 22132E 26294.00447575 .00234852 23273-4 62243-3 0 9991
|
||||
2 54039 97.2810 308.3797 0003381 332.0024 28.1069 15.94659215225828
|
||||
GSLV R/B
|
||||
1 54149U 22138AN 26294.14659811 .00002004 00000+0 16776-3 0 9992
|
||||
2 54149 87.3731 90.6054 0023139 55.0936 305.2466 14.97702774217660
|
||||
XRISM
|
||||
1 57800U 23137A 26294.26863262 .00005489 00000+0 29450-3 0 9992
|
||||
2 57800 31.0022 262.3157 0007616 159.4646 200.6300 15.13378384171970
|
||||
ACS3
|
||||
1 59588U 24077B 26294.15446671 .00016687 00000+0 11432-1 0 9991
|
||||
2 59588 97.2537 185.7246 0055915 12.4986 347.7555 13.98811813125623
|
||||
SPACEMOBILE-003
|
||||
1 61045U 24163A 26293.75464403 -.00001138 00000+0 -36856-4 0 9993
|
||||
2 61045 52.9706 92.1585 0006545 125.8552 234.3051 15.20289716116877
|
||||
SPACEMOBILE-005
|
||||
1 61046U 24163B 26294.13337173 .00002573 00000+0 13140-3 0 9992
|
||||
2 61046 52.9803 90.4331 0009987 124.9536 235.2397 15.20370604116927
|
||||
SPACEMOBILE-001
|
||||
1 61047U 24163C 26293.73782963 -.00000334 00000+0 -28569-6 0 9998
|
||||
2 61047 52.9754 91.9124 0007715 126.2538 233.9170 15.20332074116869
|
||||
SPACEMOBILE-002
|
||||
1 61048U 24163D 26293.78790289 .00015439 00000+0 71334-3 0 9999
|
||||
2 61048 52.9748 91.7680 0007035 115.3506 244.8217 15.20360596116871
|
||||
SPACEMOBILE-004
|
||||
1 61049U 24163E 26293.63881022 .00016281 00000+0 75183-3 0 9992
|
||||
2 61049 52.9685 92.2050 0008174 150.7184 209.4270 15.20331535116854
|
||||
CZ-8A R/B
|
||||
1 66004U 25231K 26294.17425587 .00000019 00000+0 65100-4 0 9996
|
||||
2 66004 50.0047 223.2832 0116769 307.9803 51.0612 14.30194669 52977
|
||||
SZ-21 MODULE
|
||||
1 66515U 25246C 26294.22802189 .00131005 13520-4 39861-3 0 9992
|
||||
2 66515 41.4681 234.9893 0012983 217.8317 142.1621 15.91235557 53715
|
||||
SPACEMOBILE-006
|
||||
1 67232U 25309A 26293.79566921 .00002943 00000+0 15370-3 0 9992
|
||||
2 67232 52.9950 116.0791 0011496 152.3089 207.8519 15.19052812 45685
|
||||
SPACEMOBILE-008
|
||||
1 69589U 26139A 26294.31718713 .00001568 00000+0 10443-3 0 9996
|
||||
2 69589 52.9961 49.8238 0003612 12.0577 348.0500 15.13083337 19070
|
||||
SPACEMOBILE-009
|
||||
1 69590U 26139B 26293.59929539 .00000948 00000+0 69967-4 0 9994
|
||||
2 69590 52.9942 53.1631 0003674 359.7665 0.3323 15.12974974 18966
|
||||
SPACEMOBILE-010
|
||||
1 69591U 26139C 26293.81017468 .00002922 00000+0 18114-3 0 9990
|
||||
2 69591 52.9943 52.3473 0003542 352.9751 7.1190 15.12844296 19004
|
||||
+290
@@ -0,0 +1,290 @@
|
||||
<!doctype html>
|
||||
<html lang="en"><head><meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MRS Telescope</title>
|
||||
<style>
|
||||
:root{--bg:#080b12;--panel:#101621;--panel2:#0c111a;--line:#222c3c;--tx:#e8edf6;--mut:#8491a6;--ac:#397dff;--ok:#42d392;--bad:#ff5968;--warn:#ffc247}
|
||||
*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0}body{background:var(--bg);color:var(--tx);font:13px system-ui,sans-serif;display:flex;flex-direction:column;gap:8px;padding:8px;overflow:hidden}
|
||||
#statusbar{height:42px;min-height:42px;display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:12px;padding:0 12px;background:#0d121b;border:1px solid #1d2736;border-radius:10px;font-variant-numeric:tabular-nums}
|
||||
.status-side{display:flex;align-items:center;gap:14px;min-width:0}.status-side.right{justify-content:flex-end}.status-item{display:flex;align-items:center;gap:7px;white-space:nowrap}.status-center{display:flex;align-items:center;gap:9px;white-space:nowrap}.status-center strong{font-size:13px;font-weight:650}.status-center span{font-size:12px;color:var(--mut)}#statusCamera{font-size:11px;color:#b5c0d1;overflow:hidden;text-overflow:ellipsis}
|
||||
.gps-icon{width:16px;height:16px;flex:none}.gps-item.no-fix{color:var(--bad)}.gps-item.has-fix{color:#fff}.gps-sats{font-size:12px;font-weight:650}
|
||||
.battery-ring{width:30px;height:30px;position:relative;display:flex;align-items:center;justify-content:center;flex:none}.battery-ring svg{position:absolute;inset:0;width:100%;height:100%;transform:rotate(-90deg)}.battery-ring circle{fill:none;stroke-width:3.2}.battery-ring .track{stroke:#30394a}.battery-ring .level{stroke:#f0f3f8;stroke-linecap:round;transition:stroke-dasharray .25s,stroke .25s}.battery-ring.low .level{stroke:var(--bad)}.battery-ring.charging .level{stroke:var(--ac)}.battery-ring.charging::after{content:"";position:absolute;inset:5px;border:2px solid transparent;border-top-color:#397dff;border-radius:50%;animation:charge-spin 1s linear infinite}.battery-value{position:relative;z-index:1;font-size:8px;font-weight:750;line-height:1;color:#fff;display:flex;align-items:center;justify-content:center;width:100%;height:100%;text-align:center}.battery-ring.charging .battery-value{color:#fff}@keyframes charge-spin{to{transform:rotate(360deg)}}
|
||||
#workspace{flex:1;min-height:0;display:grid;grid-template-columns:minmax(0,1fr) minmax(290px,360px);gap:8px}#cameraArea{display:flex;flex-direction:column;gap:8px;min-width:0;min-height:0}#view{position:relative;flex:1;min-height:160px;width:100%;background:#000;border:1px solid var(--line);border-radius:12px;overflow:hidden}#view img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:0;pointer-events:none}#ov{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;z-index:1;pointer-events:auto;cursor:crosshair}#pip{position:absolute;top:12px;right:12px;width:min(24%,210px);height:auto;aspect-ratio:1;border:1px solid #28344a;border-radius:50%;background:#060a14dd}#viewTools{display:none;position:absolute;left:10px;right:10px;bottom:10px;justify-content:center;pointer-events:none}#viewTools #ovc{display:flex;align-items:center;justify-content:center;gap:5px;flex-wrap:wrap;pointer-events:auto;background:#0b111bdc;border:1px solid #283246;border-radius:9px;padding:5px;max-width:100%;backdrop-filter:blur(12px)}
|
||||
button,input{font:inherit;color:var(--tx)}button{background:#171f2d;border:1px solid #2a3547;border-radius:7px;padding:6px 9px;cursor:pointer}button:hover:not(:disabled){border-color:var(--ac)}button:disabled{opacity:.35;cursor:not-allowed}button.p,.chip.on,.segs button.on{background:var(--ac);border-color:var(--ac)}.chip{border-radius:99px;padding:5px 10px;font-size:11px}.segs{display:flex;gap:4px}.segs button{flex:1;min-width:0;padding:6px 5px;font-size:12px}.ctl{margin:8px 0}.lh{display:flex;justify-content:space-between;align-items:center;gap:8px;color:var(--mut);margin-bottom:5px}.lh b{color:var(--tx);font-weight:600}input[type=text],input[type=number]{background:#0b111b;border:1px solid var(--line);border-radius:7px;padding:7px 8px;width:100%;min-width:0}input[type=range]{width:100%;accent-color:var(--ac);height:18px;margin:0}#cam.af input[data-k=lensPosition]{opacity:.35;pointer-events:none}details summary{cursor:pointer;color:#b5c0d1;margin:8px 0}.flips{display:flex;gap:4px;flex-wrap:wrap}.g2{display:grid;grid-template-columns:1fr 1fr;gap:6px}.big{font-size:20px;font-weight:600}.mut{color:var(--mut)}.ok{color:var(--ok)}.bad{color:var(--bad)}
|
||||
#cameraDock{display:flex;flex-direction:column;gap:8px;align-items:stretch;padding:10px 12px;background:#101621;border:1px solid var(--line);border-radius:12px;max-height:42vh;overflow:auto;scrollbar-width:thin}#captureControls{display:grid;grid-template-columns:1fr;grid-template-areas:"settings" "fields" "modes" "shutter" "job";align-items:center;gap:7px;min-width:0;border:0;padding:0}#capTabs{grid-area:modes;width:min(100%,360px);justify-self:center}#capFields{grid-area:fields;width:min(100%,360px);justify-self:center}#capGo{grid-area:shutter;width:58px;height:58px;justify-self:center}#capJob{grid-area:job}#quickSettings{grid-area:settings;display:flex;align-items:center;justify-content:flex-start;gap:7px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;white-space:nowrap;scrollbar-width:none;padding:2px 1px;max-width:100%;touch-action:pan-x}#quickSettings::-webkit-scrollbar{display:none}#quickSettings button{flex:0 0 auto}#quickSettings button{background:#0b111b;border-color:#273247;border-radius:8px;min-width:68px;padding:7px 10px}#quickSettings button b{display:block;font-size:12px;color:#fff}#quickSettings button span{display:block;font-size:9px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em}#capTabs{width:100%}#capFields{width:100%}#capFields .ctl{margin:5px 0}.shut{width:42px;height:42px;border-radius:50%;border:3px solid #e9edf4;background:#f04455;padding:0;flex:none}.shut.rec{border-radius:10px}#capJob{font-size:11px;text-align:center}#cameraSettings{min-width:0}#cameraSettings>.lh{display:none}#cameraSettings #cam{display:none}#cameraSettings.open #cam{display:grid}#skyCard .sky-layout{display:grid;grid-template-columns:minmax(0,1fr);gap:10px}#pip{position:relative;top:auto;right:auto;width:min(100%,260px);height:auto;aspect-ratio:1;border:1px solid #28344a;border-radius:50%;background:#060a14dd;justify-self:center}#skyCard #ovc{display:flex;gap:5px;flex-wrap:wrap}#skyCard .chip{background:#171f2d}#skyCard .chip.on{background:var(--ac)}#skyCard #bodyMenu{position:static;transform:none;display:none;flex-wrap:wrap;box-shadow:none;margin-top:7px}#skyCard #bodyMenu.open{display:flex}.mount-pad{position:absolute;z-index:5;pointer-events:auto;top:10px;left:10px;display:grid;grid-template-columns:repeat(3,28px);grid-template-rows:repeat(3,28px);gap:3px;padding:5px;background:#080b12a8;border:1px solid #ffffff30;border-radius:10px;backdrop-filter:blur(8px)}.mount-pad button{width:28px;height:28px;padding:0;background:#111827bb;border-color:#ffffff26}.mount-pad .center{font-size:10px;color:var(--mut)}.mount-pad .focus{font-size:9px;color:#b8c7df}.mount-pad .tracked{background:var(--ac);color:#fff;border-color:var(--ac)}#skyCard .overlay-group{background:transparent!important;border-radius:0;padding:8px 0}#skyCard .overlay-group .chip{background:#171f2d}#cameraSettings #cam{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0 12px}#cameraSettings #cam details{grid-column:1/-1}#cameraSettings #cam .ctl{margin:5px 0}#cameraSettings #cam .ctl .lh{font-size:11px}#cameraSettings #cam .segs button{font-size:10px;padding:5px 2px}
|
||||
#rightpanel{display:flex;flex-direction:column;gap:8px;min-width:0;min-height:0;overflow:auto;scrollbar-width:thin}section{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:12px;min-width:0;flex:none}h3{margin:0 0 10px;font-size:11px;letter-spacing:.09em;text-transform:uppercase;color:var(--mut);display:flex;align-items:center;justify-content:space-between;gap:8px}h3 small{text-transform:none;letter-spacing:0}.dpad{display:grid;grid-template-columns:repeat(3,34px);gap:4px}.dpad button{height:34px;padding:0}.mount-controls{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}.mount-controls>.lh{grid-column:1/-1}.mount-move{grid-column:1/-1;display:flex;align-items:center;justify-content:center;gap:20px;padding:2px 0}.mount-controls .ctl{margin:0}.controls-layout{display:flex;flex-direction:column;gap:12px}.camera-controls{border-top:1px solid var(--line);padding-top:10px}.target-info{display:grid;grid-template-columns:auto 1fr;gap:0 12px}.target-info>div{padding:5px 0;border-bottom:1px solid #253044}.target-info>div:nth-child(odd){color:var(--mut)}#tInfo .bad{margin-top:6px}#align{font-size:11px;line-height:1.4}table{width:100%;border-collapse:collapse;table-layout:auto}td,th{padding:7px 4px;text-align:left;font-weight:400;border-bottom:1px solid #253044;vertical-align:top}tr.rec{cursor:pointer}tr.rec:hover,tr.sel{background:#182438}#recs th{color:var(--mut);font-size:10px;text-transform:uppercase;letter-spacing:.05em}#recs td{font-size:12px}#recs td:last-child{color:var(--mut)}#toast{position:fixed;bottom:14px;left:14px;background:#2a1018;border:1px solid var(--bad);padding:8px 12px;border-radius:8px;display:none;z-index:9}
|
||||
#bodyMenu{display:none;position:absolute;bottom:56px;left:50%;transform:translateX(-50%);padding:8px;background:#0b111bf2;border:1px solid #2a3547;border-radius:10px;box-shadow:0 10px 28px #0008;gap:5px;z-index:5;white-space:nowrap}#bodyMenu.open{display:flex}#bodyMenu .chip{background:#171f2d}#bodyMenu .chip.on{background:var(--ac)}#view{isolation:isolate}.pan-zone{position:absolute;z-index:2;pointer-events:auto;display:flex;align-items:center;justify-content:center;background:#0b111b88;color:#e8edf6;border:1px solid #ffffff1c;opacity:0;transition:opacity .15s;user-select:none;touch-action:none}.pan-zone:hover,#view.pan-active .pan-zone{opacity:.9}.pan-zone.n,.pan-zone.s{left:40%;right:40%;height:18%;cursor:ns-resize}.pan-zone.n{top:0;border-radius:0 0 9px 9px}.pan-zone.s{bottom:0;border-radius:9px 9px 0 0}.pan-zone.e,.pan-zone.w{top:35%;bottom:35%;width:12%;cursor:ew-resize}.pan-zone.e{right:0;border-radius:9px 0 0 9px}.pan-zone.w{left:0;border-radius:0 9px 9px 0}.pan-zone span{font-size:20px;text-shadow:0 1px 4px #000}
|
||||
@media(min-width:1500px){#workspace{grid-template-columns:minmax(0,1fr) 360px}#cameraSettings #cam{grid-template-columns:repeat(4,minmax(0,1fr))}}
|
||||
@media(max-width:1050px){body{height:auto;min-height:100%;overflow:auto}#workspace{grid-template-columns:minmax(0,1fr) 300px}#view{flex:none;aspect-ratio:4/3}#cameraDock{max-height:none}#cameraSettings #cam{grid-template-columns:repeat(2,minmax(0,1fr))}}
|
||||
@media(max-width:760px){body{padding:6px;gap:6px}#statusbar{grid-template-columns:auto minmax(0,1fr);gap:8px;padding:0 8px}.status-side.left{display:none}#statusCamera{display:none}.status-center{justify-self:start;gap:6px}.status-center strong{font-size:12px}.status-center span{font-size:10px}.status-side.right{gap:10px}#workspace{display:flex;flex-direction:column}#view{aspect-ratio:1/1;min-height:0}#cameraDock{max-height:none;padding:9px}#captureControls{min-width:0}#capGo{width:52px;height:52px}#cameraSettings #cam{grid-template-columns:repeat(2,minmax(0,1fr))}#rightpanel{overflow:visible}#pip{width:28%}#viewTools{left:5px;right:5px;bottom:5px}#viewTools #ovc{gap:3px;padding:4px}.chip{padding:4px 8px}}
|
||||
|
||||
/* Compact camera-app controls and overlay settings */
|
||||
#workspace{grid-template-columns:minmax(0,1fr) minmax(290px,360px)}
|
||||
#cameraDock{max-height:none;overflow:visible;padding:10px 12px 12px}
|
||||
#captureControls{display:flex;flex-direction:column;gap:9px}
|
||||
#quickSettings{display:flex;flex-direction:row;flex-wrap:nowrap;justify-content:center;align-items:stretch;gap:6px;width:100%;min-width:0;max-width:100%;overflow-x:auto;overflow-y:hidden;white-space:nowrap;scrollbar-width:none;overscroll-behavior-x:contain;touch-action:pan-x;padding:2px 1px 7px}
|
||||
#quickSettings button{flex:0 0 auto;min-width:66px;max-width:none;padding:7px 9px;background:#0b111b;border-color:#273247;text-align:center;white-space:normal}
|
||||
#quickSettings button.active{border-color:var(--ac);background:#172947}
|
||||
#quickSettings button span{display:block;font-size:9px;color:var(--mut);text-transform:uppercase;letter-spacing:.06em}
|
||||
#quickSettings button b{display:block;font-size:12px;color:#fff;max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
#quickSettings::-webkit-scrollbar{display:none}
|
||||
#captureRow{display:flex;align-items:center;justify-content:center;gap:18px}
|
||||
#captureRow .shut{width:58px;height:58px;border-radius:50%;border:3px solid #f4f6fb;background:#f04455}
|
||||
#captureRow .shut.rec{border-radius:12px}
|
||||
.mode-cycle{width:42px;height:42px;border:0;border-radius:50%;background:#242b37;font-size:28px;line-height:1;padding:0}
|
||||
.mode-label{text-align:center;color:#f5a623;font-size:10px;letter-spacing:.08em;margin-top:-5px}
|
||||
#capFields{display:none;position:absolute;left:50%;bottom:100px;transform:translateX(-50%);z-index:8;width:min( min(600px, calc(100% - 24px)), 600px);padding:12px 16px;background:#111923ed;border:1px solid #ffffff20;border-radius:12px;backdrop-filter:blur(14px);box-shadow:0 10px 30px #0008}
|
||||
#capFields.open{display:block}#capFields .ctl{margin:0}#capFields .lh{margin-bottom:8px}#settingOverlay{display:none;position:absolute;left:50%;bottom:.75rem;transform:translateX(-50%);z-index:20;pointer-events:auto;width:min(600px,calc(100% - 24px));padding:12px 16px;background:#111923ed;border:1px solid #ffffff20;border-radius:12px;backdrop-filter:blur(14px);box-shadow:0 10px 30px #0008}#settingOverlay.open{display:block}#settingOverlay .ctl{margin:0}#settingOverlay .setting-hidden{display:none!important}#settingOverlay .lh{margin-bottom:8px}#settingOverlay .segs{display:flex;flex-wrap:wrap;gap:5px}#settingOverlay input[type=range]{width:100%}#settingOverlay .flips{display:flex!important;gap:6px;flex-wrap:wrap}#settingOverlay button,#settingOverlay input{position:relative;z-index:1;pointer-events:auto}
|
||||
#cameraSettings{display:none!important}
|
||||
#cam{display:block!important}#cam .ctl{display:none!important}#cam .ctl.setting-open{display:block!important}
|
||||
#cam details{display:none!important}#cam .flips{display:none!important}
|
||||
#view{flex:1;min-height:240px}#view img,#ov{object-fit:contain}
|
||||
#pip{display:block!important;position:relative;top:auto;right:auto;width:min(100%,260px);aspect-ratio:1;justify-self:center}
|
||||
#skyCard .sky-layout{grid-template-columns:minmax(0,1fr)}#skyCard #ovc,#skyCard #bodyMenu{display:flex;gap:5px;flex-wrap:wrap;position:static;transform:none;box-shadow:none;background:transparent;border:0;padding:0;margin:0;white-space:normal}
|
||||
.overlay-group{padding:8px 0;border-top:1px solid #253044}.group-label{font-size:10px;color:var(--mut);text-transform:uppercase;letter-spacing:.07em;margin-bottom:7px}
|
||||
#skyCard .chip{background:#171f2d}#skyCard .chip.on{background:var(--ac)}
|
||||
@media(max-width:760px){#quickSettings{gap:5px;justify-content:flex-start;flex-wrap:nowrap;overflow-x:auto}#quickSettings button{min-width:58px;padding:6px 7px}#capFields{bottom:92px}#captureRow{gap:14px}#captureRow .shut{width:54px;height:54px}}
|
||||
</style></head><body>
|
||||
<div id="statusbar" aria-label="Device status">
|
||||
<div class="status-side left"><div class="status-item" id="statusCamera">Camera unavailable</div></div>
|
||||
<div class="status-center"><strong id="clockStatus">--:--</strong><span id="dateStatus">---</span></div>
|
||||
<div class="status-side right">
|
||||
<div class="status-item gps-item no-fix" id="gpsItem"><svg class="gps-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-label="GPS"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M1 12h4M19 12h4"/></svg><span id="gpsSats" class="gps-sats">—</span></div>
|
||||
<div class="status-item"><div id="batteryRing" class="battery-ring"><svg viewBox="0 0 36 36"><circle class="track" cx="18" cy="18" r="15"/><circle id="batteryLevel" class="level" cx="18" cy="18" r="15" pathLength="100" stroke-dasharray="0 100"/></svg><span id="batteryValue" class="battery-value">—%</span></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="workspace">
|
||||
<div id="cameraArea">
|
||||
<div id="view">
|
||||
<img id="stream" alt="live view"><canvas id="ov"></canvas><div id="capFields"></div><div id="settingOverlay"></div><div class="mount-pad"><button class="focus" data-focus="coarse-out" title="Coarse telescope focus out">C−</button><button data-pan="up">↑</button><button class="focus" data-focus="coarse-in" title="Coarse telescope focus in">C+</button><button data-pan="left">←</button><button class="center" id="trackToggle" title="Toggle target tracking" aria-label="Toggle target tracking"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M1 12h4M19 12h4"/></svg></button><button data-pan="right">→</button><button class="focus" data-focus="fine-out" title="Fine telescope focus out">F−</button><button data-pan="down">↓</button><button class="focus" data-focus="fine-in" title="Fine telescope focus in">F+</button></div>
|
||||
|
||||
</div>
|
||||
<div id="cameraDock">
|
||||
<div id="captureControls">
|
||||
<div id="quickSettings"></div>
|
||||
<div id="captureRow"><button class="mode-cycle" id="modePrev" title="Previous mode">‹</button><button class="shut" id="capGo" title="Capture"></button><button class="mode-cycle" id="modeNext" title="Next mode">›</button></div><div class="mode-label" id="modeLabel">PHOTO</div><div id="capJob" class="mut"></div>
|
||||
</div>
|
||||
<div id="cameraSettings"><div class="lh"><b>Camera settings</b><button id="camReset" style="padding:3px 8px">Reset</button></div><div id="cam"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="rightpanel">
|
||||
<section id="skyCard"><h3>Sky map & overlays</h3><div class="sky-layout"><canvas id="pip" width="420" height="420"></canvas><div><div class="overlay-group"><div class="group-label">Camera overlay</div><div id="ovc"></div></div><div class="overlay-group"><div class="group-label">Bodies</div><div id="bodyMenu"><button class="chip on" data-body="stars">Stars</button><button class="chip on" data-body="dso">Deep sky</button><button class="chip on" data-body="terrestrial">Planets / Moon / Sun</button></div></div></div></div></section>
|
||||
<section id="targetCard"><h3>Target <small id="tName"></small></h3>
|
||||
<div class="g2"><input id="tIn" type="text" placeholder="moon, vega, iss, sat:25544…"><button class="p" id="tSet">Set</button></div>
|
||||
<div id="tInfo" style="margin-top:6px"></div>
|
||||
<div class="g2" style="margin-top:8px"><button id="zero">Zero on target</button><button id="unzero">Clear alignment</button></div><div id="align" class="mut" style="margin-top:6px"></div></section>
|
||||
<section><h3>Recommended now</h3><table id="recs"></table></section>
|
||||
</div>
|
||||
</div>
|
||||
<div id="toast"></div>
|
||||
<script>
|
||||
const $=(i)=>document.getElementById(i),rad=Math.PI/180,wrap180=(a)=>((a+540)%360)-180;
|
||||
const ST=(k,d)=>{try{return JSON.parse(localStorage.getItem(k))??d}catch{return d}},SV=(k,v)=>{try{localStorage.setItem(k,JSON.stringify(v))}catch{}};
|
||||
const S={pt:{az:0,alt:0,aligned:false,declination:{deg:0}},realPoint:false,delta:null,targetInfo:null,objs:[],constellationLines:[],target:ST('target',''),cap:'single',cam:null,st:null},O={...{labels:1,stars:1,dso:1,terrestrial:1,cross:1,err:1,map:1},...ST('ov',{})};
|
||||
const j=async(u,o)=>{const r=await fetch(u,o&&{...o,headers:{'Content-Type':'application/json'},body:o.body&&JSON.stringify(o.body)});const d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.message||d.error||r.status);return d};
|
||||
const toast=(m)=>{const t=$('toast');t.textContent=m;t.style.display='block';clearTimeout(toast.t);toast.t=setTimeout(()=>t.style.display='none',4000)};
|
||||
const act=(f)=>async(...a)=>{try{await f(...a)}catch(e){toast(e.message)}};
|
||||
const tm=(s)=>s?new Date(s).toLocaleTimeString([],{hour:'numeric',minute:'2-digit'}):'—';
|
||||
$('stream').src='/stream';
|
||||
// Edge taps and drags are ready for mount integration; this build has no mount command endpoint exposed.
|
||||
const panHint=()=>toast('Mount hardware is not connected');
|
||||
$('view').querySelectorAll('[data-pan],[data-focus]').forEach((zone)=>zone.addEventListener('click',(e)=>{e.stopPropagation();panHint()}));
|
||||
let tracking=false;$('trackToggle').onclick=(e)=>{e.stopPropagation();tracking=!tracking;$('trackToggle').classList.toggle('tracked',tracking);$('trackToggle').title=tracking?'Disable target tracking':'Enable target tracking';toast(tracking?'Target tracking enabled in UI':'Target tracking disabled in UI')};
|
||||
let panStart=null;
|
||||
$('view').addEventListener('pointerdown',(e)=>{if(e.target!==$('ov'))return;panStart={x:e.clientX,y:e.clientY};e.target.setPointerCapture?.(e.pointerId)});
|
||||
$('view').addEventListener('pointerup',(e)=>{if(!panStart)return;const dx=e.clientX-panStart.x,dy=e.clientY-panStart.y;panStart=null;if(Math.hypot(dx,dy)>18)panHint()});
|
||||
$('view').addEventListener('pointercancel',()=>{panStart=null});
|
||||
|
||||
// ---------- camera controls (camera-app style) ----------
|
||||
const SH=[0,250,500,1e3,2e3,4e3,8e3,16e3,33333,66666,125e3,25e4,5e5,1e6,2e6,5e6,1e7],IS=[0,100,200,400,800,1600,3200,6400],f2=(x)=>(+x).toFixed(2);
|
||||
const fs=(u)=>u?(u>=1e6?u/1e6+' s':'1/'+Math.round(1e6/u)):'Auto';
|
||||
const MAIN=[['shutterUs','Shutter','steps',SH,fs],['iso','ISO','steps',IS,(x)=>x||'Auto'],['ev','Exposure comp','range',-4,4,.33,(x)=>(x>0?'+':'')+(+x).toFixed(1)+' EV'],
|
||||
['autofocusMode','Focus','seg',['manual','auto','continuous']],['lensPosition','Lens position','range',0,12,.1,(x)=>(+x).toFixed(1)+' D'],
|
||||
['awb','White balance','seg',['auto','daylight','cloudy','tungsten','fluorescent']]];
|
||||
const MORE=[['brightness','Brightness','range',-1,1,.05,f2],['contrast','Contrast','range',0,4,.05,f2],['saturation','Saturation','range',0,4,.05,f2],['sharpness','Sharpness','range',0,8,.25,f2],['quality','JPEG quality','range',1,101,1,(x)=>Number(x)>100?'RAW':x],
|
||||
['denoise','Denoise','seg',['auto','off','cdn_fast','cdn_hq']],['exposureMode','Exposure mode','seg',['normal','sport','long']],['rotation','Rotation','seg',[0,180]]];
|
||||
const ALL=[...MAIN,...MORE];
|
||||
const M={};
|
||||
const ctl=(r)=>{const[k,l,t]=r;
|
||||
if(t==='seg')return`<div class="ctl" data-setting="${k}"><div class="lh">${l}</div><div class="segs" data-k="${k}">${r[3].map((o)=>`<button data-v="${o}">${o}</button>`).join('')}</div></div>`;
|
||||
M[k]=t==='steps'?{vals:r[3],f:r[4],min:0,max:r[3].length-1,step:1}:{min:r[3],max:r[4],step:r[5],f:r[6]};const m=M[k];
|
||||
return`<div class="ctl" data-setting="${k}"><div class="lh"><span>${l}</span><b id="v_${k}"></b></div><input type="range" data-k="${k}" min="${m.min}" max="${m.max}" step="${m.step}"></div>`};
|
||||
$('cam').innerHTML=ALL.map(ctl).join('')+`<div class="flips segs" data-setting="rotation-flips"><button data-k="hflip">Mirror Horizontal</button><button data-k="vflip">Mirror Vertical</button></div>`;
|
||||
const fmtSetting=(r,s)=>{const k=r[0],v=s?.[k];if(k==='shutterUs')return fs(v);if(k==='iso')return v||'Auto';if(k==='ev')return (Number(v)>0?'+':'')+Number(v||0).toFixed(1);if(k==='autofocusMode')return v||'Auto';if(k==='lensPosition')return s?.autofocusMode==='manual'?(Number(v)||0).toFixed(1)+' D':'Auto';if(k==='awb'||k==='denoise'||k==='exposureMode')return String(v??'Auto');if(k==='quality')return String(v??'—');if(k==='rotation')return String(v??0)+'°';return r[6]?r[6](v??0):String(v??'—')};
|
||||
const quickOrder=[...ALL.filter((r)=>!['quality','lensPosition','exposureMode'].includes(r[0])),...ALL.filter((r)=>r[0]==='quality')];$('quickSettings').innerHTML=quickOrder.map((r)=>{const k=r.key||r[0],label=r.label||r[1].replace('Exposure comp','EV').replace('White balance','WB').replace('Lens position','Lens').replace('JPEG quality','Compress');return `<button data-quick="${k}"><span>${label}</span><b id="quick_${k}">—</b></button>`}).join('');
|
||||
let activeSetting=null;
|
||||
function restoreSettings(){const fields=$('settingOverlay');document.querySelectorAll('#settingOverlay [data-setting]').forEach((el)=>{$('cam').append(el);el.classList.remove('setting-open','setting-hidden')});fields.classList.remove('open');fields.replaceChildren();activeSetting=null}
|
||||
function openSetting(k){const fields=$('settingOverlay');if(activeSetting?.dataset.setting===k){restoreSettings();document.querySelectorAll('[data-quick]').forEach((b)=>b.classList.remove('active'));return}restoreSettings();document.querySelectorAll('[data-quick]').forEach((b)=>b.classList.remove('active'));const field=document.querySelector(`#cam [data-setting="${CSS.escape(k)}"]`);if(!field)return;activeSetting=field;field.classList.add('setting-open');fields.append(field);
|
||||
if(k==='autofocusMode'){const lens=document.querySelector('#cam [data-setting="lensPosition"]');if(lens){lens.classList.add('setting-open');fields.append(lens);lens.classList.toggle('setting-hidden',S.cam?.autofocusMode!=='manual')}}
|
||||
if(k==='shutterUs'){const exposure=document.querySelector('#cam [data-setting="exposureMode"]');if(exposure){exposure.classList.add('setting-open');fields.append(exposure)}}
|
||||
if(k==='rotation'){const flips=document.querySelector('#cam [data-setting="rotation-flips"]');if(flips)fields.append(flips)}
|
||||
fields.classList.add('open');document.querySelector(`[data-quick="${CSS.escape(k)}"]`)?.classList.add('active')}
|
||||
$('quickSettings').addEventListener('click',(e)=>{const b=e.target.closest('[data-quick]');if(b)openSetting(b.dataset.quick)});
|
||||
function paint(){const s=S.cam;if(!s)return;document.querySelectorAll('#cam [data-k], #settingOverlay [data-k]').forEach((el)=>{const k=el.dataset.k,v=s[k];if(el.type==='range'){const m=M[k];el.value=k==='quality'&&S.raw?101:(m.vals?m.vals.reduce((b,x,i)=>Math.abs(x-v)<Math.abs(m.vals[b]-v)?i:b,0):(v??0));const label=$('v_'+k);if(label)label.textContent=k==='quality'&&S.raw?'RAW':m.f(m.vals?m.vals[el.value]:+el.value)}else if(el.classList.contains('segs'))el.querySelectorAll('button').forEach((b)=>b.classList.toggle('on',b.dataset.v==String(v)));else if(k==='hflip'||k==='vflip')el.classList.toggle('on',!!v)});ALL.forEach((r)=>{const el=$('quick_'+r[0]);if(el)el.textContent=r[0]==='quality'?(S.raw?'RAW':String(s.quality??'—')):fmtSetting(r,s)});const lens=document.querySelector('#settingOverlay [data-setting="lensPosition"]');if(lens)lens.classList.toggle('setting-hidden',s.autofocusMode!=='manual')}
|
||||
const tmr={};function send(k,v){if(!S.cam)return;S.cam[k]=v;clearTimeout(tmr[k]);tmr[k]=setTimeout(async()=>{try{const result=await j('/api/camera/settings',{method:'PATCH',body:{[k]:v}});S.cam=result.settings;paint()}catch(error){toast(error.message)}},250)}
|
||||
document.addEventListener('input',(e)=>{const el=e.target,k=el.dataset.k;if(el.type!=='range'||!k)return;const m=M[k],v=m.vals?m.vals[+el.value]:+el.value;if(k==='quality'&&v>100){S.raw=true;$('v_quality').textContent='RAW';paint();return}if(k==='quality')S.raw=false;$('v_'+k).textContent=m.f(v);send(k,v);if(k==='autofocusMode')paint()});
|
||||
document.addEventListener('change',(e)=>{const el=e.target;if(el.type==='range'&&el.dataset.k==='quality'&&+el.value>100){S.raw=true;paint()}});
|
||||
document.addEventListener('click',(e)=>{const b=e.target.closest('button');if(!b)return;const k=(b.closest('[data-k]')||b).dataset.k;if(!S.cam)return;if(b.dataset.v!==undefined){if(!k)return;send(k,k==='rotation'?Number(b.dataset.v):b.dataset.v);paint()}else if(k==='hflip'||k==='vflip'){send(k,!S.cam[k]);paint()}});
|
||||
$('camReset').onclick=act(async()=>{S.cam=(await j('/api/camera/settings/reset',{method:'POST',body:{}})).settings;paint()});
|
||||
|
||||
// ---------- capture ----------
|
||||
const CAPF={single:'',
|
||||
timelapse:`<div class="g2"><div class="ctl"><div class="lh">Interval (s)</div><input type="number" id="tlI" value="10"></div><div class="ctl"><div class="lh">Frames</div><input type="number" id="tlN" value="180"></div></div>`,
|
||||
video:`<div class="g2"><div class="ctl"><div class="lh">Length (s, 0 = manual stop)</div><input type="number" id="vD" value="0"></div><div class="ctl"><div class="lh">FPS</div><input type="number" id="vF" value="30"></div></div>`};
|
||||
const MODES=['single','video','timelapse'],modeNames={single:'PHOTO',video:'VIDEO',timelapse:'TIMELAPSE'};const capUI=()=>{$('capFields').innerHTML=CAPF[S.cap];$('modeLabel').textContent=modeNames[S.cap];$('modePrev').textContent=modeNames[MODES[(MODES.indexOf(S.cap)+MODES.length-1)%MODES.length]].slice(0,1);$('modeNext').textContent=modeNames[MODES[(MODES.indexOf(S.cap)+1)%MODES.length]].slice(0,1);$('capFields').classList.remove('open')};const cycleMode=(d)=>{S.cap=MODES[(MODES.indexOf(S.cap)+d+MODES.length)%MODES.length];capUI()};$('modePrev').onclick=()=>cycleMode(-1);$('modeNext').onclick=()=>cycleMode(1);capUI();
|
||||
const nv=(i)=>Number($(i).value);
|
||||
$('capGo').onclick=act(async()=>{const job=S.st?.camera?.job;
|
||||
if(job?.type==='video')return j('/api/camera/video/stop',{method:'POST',body:{}});
|
||||
if(job?.type==='timelapse')return j('/api/camera/timelapse/stop',{method:'POST',body:{}});
|
||||
if(S.cap==='single'){const r=await j('/api/camera/photo',{method:'POST',body:{raw:!!S.raw}});toast('Saved '+r.name)}
|
||||
else if(S.cap==='timelapse')await j('/api/camera/timelapse/start',{method:'POST',body:{intervalSec:nv('tlI'),count:nv('tlN')}});
|
||||
else await j('/api/camera/video/start',{method:'POST',body:{durationSec:nv('vD'),fps:nv('vF')}})});
|
||||
|
||||
// ---------- target ----------
|
||||
$('tIn').value=S.target;
|
||||
$('tSet').onclick=()=>setTarget($('tIn').value.trim());
|
||||
function setTarget(t){S.target=t;SV('target',t);$('tIn').value=t;S.delta=null;refreshInfo();loadRecs();tick()}
|
||||
$('zero').onclick=act(async()=>{const r=await j('/api/celestial/zero',{method:'POST',body:{target:S.target}});toast(`Aligned. Correction ${r.correction?.separation??'—'}°`)});
|
||||
$('unzero').onclick=act(()=>j('/api/celestial/zero',{method:'DELETE'}));
|
||||
async function refreshInfo(){if(!S.target){S.targetInfo=null;$('tInfo').innerHTML='<div class="mut">Pick a recommendation or type a target.</div>';return}
|
||||
try{const i=await j('/api/sky/info?target='+encodeURIComponent(S.target)),e=i.events;S.targetInfo=i;$('tName').textContent=i.target.label;
|
||||
const kv=(a,b)=>`<div>${a}</div><div>${b??'—'}</div>`;
|
||||
$('tInfo').innerHTML=`<div class="target-info">${kv('Altitude / Azimuth',i.alt+'° / '+i.az+'°')}${i.magnitude!=null?kv('Magnitude',i.magnitude)+kv('Distance',i.distanceKm!=null?(i.distanceKm/1e6).toFixed(1)+'M km ('+i.distanceAu+' AU)':'—')+kv('Illuminated',i.phasePct+'%')+kv('Elongation',i.elongationDeg+'°'):''}${i.oppositionPct!=null?kv('Opposition',i.oppositionPct+'% · '+new Date(i.nextOpposition).toLocaleDateString()):''}${e?kv('Rise / Transit / Set',tm(e.rise)+' / '+tm(e.transit)+' / '+tm(e.set)):''}${i.rangeKm?kv('Range',i.rangeKm+' km'):''}</div>${i.aboveHorizon?'':'<div class="bad">Below the horizon</div>'}`}
|
||||
catch(e){$('tInfo').innerHTML=`<div class="bad">${e.message}</div>`}}
|
||||
|
||||
// ---------- status polling / render ----------
|
||||
const ring=(p,c)=>`<svg viewBox="0 0 36 36" width="54"><circle cx="18" cy="18" r="15.9" fill="none" stroke="#1b2440" stroke-width="3.5"/><circle cx="18" cy="18" r="15.9" fill="none" stroke="${c}" stroke-width="3.5" pathLength="100" stroke-dasharray="${p} 100" stroke-linecap="round" transform="rotate(-90 18 18)"/><text x="18" y="21" text-anchor="middle" font-size="9" fill="#e6ebf7">${p}%</text></svg>`;
|
||||
async function tick(){
|
||||
try{S.st=await j('/api/status');const p=S.st.pointing;S.realPoint=!!(p&&p.az!=null);S.pt=S.realPoint?p:{az:0,alt:0,aligned:false,declination:{deg:0}};render(S.st)}catch{return}
|
||||
if(S.target&&S.realPoint)try{S.delta=await j('/api/celestial/delta?target='+encodeURIComponent(S.target))}catch{S.delta=null}else S.delta=null;
|
||||
draw();pip()}
|
||||
function render(st){
|
||||
const t=st.telemetry||{},c=st.camera||{},b=t.battery||{},g=t.gps||{},a=st.celestial?.alignment||{};
|
||||
const cs=c.settings||{};
|
||||
if(!S.cam&&Object.keys(cs).length){S.cam=cs;paint()}
|
||||
const jb=c.job;$('capJob').textContent=jb?(jb.type==='timelapse'?`Timelapse ${jb.frames}/${jb.total??'∞'}`:`Recording ${jb.elapsedSec}s`):(c.lastCapture?`Last: ${c.lastCapture.type}`:'');
|
||||
$('capGo').classList.toggle('rec',!!jb);$('capGo').title=jb?'Stop '+jb.type:'Capture';
|
||||
$('align').textContent=a.aligned?`Aligned on ${a.target?.label} at ${new Date(a.createdAt).toLocaleTimeString()}`:'Not aligned: centre a known object and press Zero.';
|
||||
const fix=String(g?.state||'').startsWith('fix'),sat=g?.satellites;
|
||||
const batteryRing=$('batteryRing');
|
||||
batteryRing.classList.toggle('low',b.available&&b.percent<20);batteryRing.classList.toggle('charging',!!b.charging);
|
||||
const batteryPct=b?.available?Math.max(0,Math.min(100,Number(b.percent)||0)):0;
|
||||
$('batteryLevel').setAttribute('stroke-dasharray',`${batteryPct} 100`);
|
||||
$('batteryValue').textContent=b?.available?`${Math.round(batteryPct)}%`:'—';
|
||||
batteryRing.title=b?.available?`${b.voltageV} V${b.charging?' · charging':''}`:'Battery unavailable';
|
||||
$('gpsItem').className='status-item gps-item '+(fix?'has-fix':'no-fix');
|
||||
$('gpsSats').textContent=String(sat?.seen??sat?.used??0);$('gpsSats').title=`${sat?.used??0} used / ${sat?.seen??0} seen · ${String(g?.state||'unknown').replaceAll('_',' ')}`;
|
||||
const now=new Date(t?.time?.epochMs||Date.now());
|
||||
$('clockStatus').textContent=now.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
|
||||
$('dateStatus').textContent='· '+now.toLocaleDateString([], {weekday:'short',month:'short',day:'numeric'});
|
||||
const stream=c.stream||{};$('statusCamera').textContent=`${stream.running?`${stream.width}×${stream.height} · ${Number(stream.fps||0).toFixed(0)} fps`:'Stream idle'} · ${fs(cs.shutterUs)} · ISO ${cs.iso||'Auto'}`;
|
||||
}
|
||||
|
||||
// Fixed field of view keeps overlays testable without IMU hardware; no synthetic bodies are injected.
|
||||
const fovDeg=()=>8;
|
||||
const OVL=[['cross','Crosshair'],['err','Error line'],['labels','Labels'],['map','Sky overlay']];
|
||||
$('ovc').innerHTML=OVL.map(([k,l])=>`<button class="chip ${O[k]?'on':''}" data-k="${k}">${l}</button>`).join('');
|
||||
function syncBodyMenu(){$$('bodyMenu button').forEach((b)=>b.classList.toggle('on',!!O[b.dataset.body]))}
|
||||
syncBodyMenu();
|
||||
function $$(selector){return document.querySelectorAll(selector)}
|
||||
$('ovc').onclick=(e)=>{const b=e.target.closest('button');if(!b)return;const k=b.dataset.k;O[k]=O[k]?0:1;SV('ov',O);b.classList.toggle('on',!!O[k]);draw();pip()};
|
||||
$('bodyMenu').onclick=(e)=>{const b=e.target.closest('button[data-body]');if(!b)return;const k=b.dataset.body;O[k]=O[k]?0:1;SV('ov',O);b.classList.toggle('on',!!O[k]);draw();pip()};
|
||||
function objectKind(o){
|
||||
const type=String(o.type||o.category||'').toLowerCase();
|
||||
if(/star|stellar/.test(type))return 'stars';
|
||||
if(/galaxy|nebula|cluster|globular|deep.?sky|\bdso\b/.test(type))return 'dso';
|
||||
if(/planet|moon|sun|terrestrial/.test(type))return 'terrestrial';
|
||||
return 'other';
|
||||
}
|
||||
function objectPosition(o){
|
||||
const az=Number(o.az??o.azimuth),alt=Number(o.alt??o.altitude);
|
||||
return Number.isFinite(az)&&Number.isFinite(alt)?{az,alt}:null;
|
||||
}
|
||||
function draw(){
|
||||
const c=$('ov'),g=c.getContext('2d'),W=c.width=Math.max(1,c.clientWidth),H=c.height=Math.max(1,c.clientHeight);
|
||||
g.clearRect(0,0,W,H);
|
||||
const P=S.pt||{az:0,alt:0},ppd=W/fovDeg(),cx=W/2,cy=H/2;
|
||||
const proj=(o)=>[cx+wrap180(o.az-P.az)*Math.cos(P.alt*rad)*ppd,cy-(o.alt-P.alt)*ppd];
|
||||
g.font='12px system-ui';g.textBaseline='middle';g.lineWidth=1.5;
|
||||
// Draw faint constellation segments when the sky API supplies explicit line pairs,
|
||||
// or star objects carry constellation + line/order metadata.
|
||||
if(O.map&&O.stars){
|
||||
g.save();g.strokeStyle='rgba(160,190,235,.28)';g.lineWidth=1;g.setLineDash([]);
|
||||
for(const line of S.constellationLines){
|
||||
const a=objectPosition(line.from||line.start||line[0]),b=objectPosition(line.to||line.end||line[1]);
|
||||
if(!a||!b)continue;const [x1,y1]=proj(a),[x2,y2]=proj(b);
|
||||
if(Math.max(x1,x2)<0||Math.min(x1,x2)>W||Math.max(y1,y2)<0||Math.min(y1,y2)>H)continue;
|
||||
g.beginPath();g.moveTo(x1,y1);g.lineTo(x2,y2);g.stroke();
|
||||
}
|
||||
const groups=new Map();
|
||||
for(const o of S.objs){if(objectKind(o)!=='stars')continue;const name=o.constellation||o.constellationName||o.constellationId;const order=Number(o.constellationOrder??o.lineOrder??o.sequence);
|
||||
if(name&&Number.isFinite(order)){if(!groups.has(String(name)))groups.set(String(name),[]);groups.get(String(name)).push({o,order})}}
|
||||
for(const stars of groups.values()){stars.sort((a,b)=>a.order-b.order);for(let i=1;i<stars.length;i++){const a=objectPosition(stars[i-1].o),b=objectPosition(stars[i].o);if(!a||!b)continue;const[x1,y1]=proj(a),[x2,y2]=proj(b);g.beginPath();g.moveTo(x1,y1);g.lineTo(x2,y2);g.stroke()}}
|
||||
g.restore();
|
||||
}
|
||||
for(const o of S.objs){
|
||||
const pos=objectPosition(o);if(!pos)continue;
|
||||
const kind=objectKind(o),visible=!!O.map&&(kind==='other'?true:!!O[kind]);if(!visible)continue;
|
||||
const[x,y]=proj(pos);if(x<-40||y<-20||x>W+40||y>H+20)continue;
|
||||
const label=String(o.label||o.name||o.spec||o.type||'Object');
|
||||
g.strokeStyle=g.fillStyle=kind==='stars'?'#dce8ff':kind==='dso'?'#ffc247':kind==='terrestrial'?'#8fb4ff':'#c5d0e6';
|
||||
g.beginPath();
|
||||
if(kind==='stars'){g.arc(x,y,Math.max(1.5,3.5-(Number(o.magnitude)||2)/2),0,Math.PI*2);g.fill()}
|
||||
else if(kind==='dso'){g.ellipse(x,y,8,5,0,0,Math.PI*2);g.stroke()}
|
||||
else{g.arc(x,y,6,0,Math.PI*2);g.stroke()}
|
||||
if(O.labels&&O.map){g.fillStyle='#e6ebf7';g.fillText(label,x+10,y)}
|
||||
}
|
||||
if(O.cross){g.strokeStyle='#ff5c6caa';g.beginPath();g.moveTo(cx-22,cy);g.lineTo(cx+22,cy);g.moveTo(cx,cy-22);g.lineTo(cx,cy+22);g.stroke()}
|
||||
if(O.err){
|
||||
const d=S.delta,rawTarget=d?.target||null;
|
||||
const target=rawTarget?.target?{...rawTarget,...rawTarget.target,label:rawTarget.target.label}:rawTarget||
|
||||
(S.targetInfo?{az:S.targetInfo.az,alt:S.targetInfo.alt,label:S.targetInfo.target?.label}:null);
|
||||
if(target&&Number.isFinite(Number(target.az))&&Number.isFinite(Number(target.alt))){
|
||||
let[x,y]=proj({az:Number(target.az),alt:Number(target.alt)});
|
||||
const off=x<0||y<0||x>W||y>H;
|
||||
g.strokeStyle=g.fillStyle=d?.onTarget?'#42d392':'#ffc247';g.setLineDash([6,5]);g.beginPath();g.moveTo(cx,cy);
|
||||
if(off){const k=.45*W/Math.max(1,Math.hypot(x-cx,y-cy));x=cx+(x-cx)*k;y=cy+(y-cy)*k}
|
||||
g.lineTo(x,y);g.stroke();g.setLineDash([]);g.beginPath();g.arc(x,y,10,0,Math.PI*2);g.stroke();
|
||||
g.fillText(`${target.label||S.target||'Target'}${off?' (off-screen)':''}`,x+14,y);
|
||||
}else if(S.target&&O.err){
|
||||
g.strokeStyle='#ffc247';g.setLineDash([6,5]);g.beginPath();g.moveTo(cx,cy);g.lineTo(cx+Math.min(W,H)*.3,cy-Math.min(W,H)*.2);g.stroke();g.setLineDash([]);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Select the nearest rendered sky object; labels are clickable too.
|
||||
$('ov').style.cursor='crosshair';$('pip').style.cursor='crosshair';
|
||||
$('view').addEventListener('click',(e)=>{
|
||||
if(e.target.closest('button, #settingOverlay, #capFields, .mount-pad'))return;
|
||||
if(!O.map||!S.objs.length)return;
|
||||
const c=$('ov'),r=c.getBoundingClientRect(),W=c.width,H=c.height,P=S.pt||{az:0,alt:0},ppd=W/fovDeg(),cx=W/2,cy=H/2;
|
||||
const x=(e.clientX-r.left)*W/r.width,y=(e.clientY-r.top)*H/r.height;
|
||||
const project=(o)=>[cx+wrap180(o.az-P.az)*Math.cos(P.alt*rad)*ppd,cy-(o.alt-P.alt)*ppd];
|
||||
let best=null,dist=65;
|
||||
for(const o of S.objs){const pos=objectPosition(o);if(!pos)continue;const kind=objectKind(o);if(!O.map||(kind==='stars'&&!O.stars)||(kind==='dso'&&!O.dso)||(kind==='terrestrial'&&!O.terrestrial))continue;
|
||||
const [ox,oy]=project(pos);if(ox<-40||oy<-20||ox>W+40||oy>H+20)continue;
|
||||
const markerDist=Math.hypot(x-ox,y-oy),label=String(o.label||o.name||o.spec||o.type||'Object');
|
||||
const inLabel=O.labels&&x>=ox+7&&x<=ox+12+label.length*7&&y>=oy-10&&y<=oy+10;const d=inLabel?0:markerDist;
|
||||
if(d<dist){dist=d;best=o}}
|
||||
if(!best){toast('Click closer to a visible sky marker or label');return}
|
||||
if(!best){toast('Click closer to a visible sky marker or label');return}const target=best.spec||best.name||best.label;if(!target)return;setTarget(String(target));toast(`Target: ${best.label||best.name||target}`);
|
||||
});
|
||||
|
||||
$('pip').addEventListener('click',(e)=>{const c=$('pip'),r=c.getBoundingClientRect(),x=(e.clientX-r.left)*420/r.width,y=(e.clientY-r.top)*420/r.height,cx=210,cy=210,R=185;let best=null,dist=18;const xy=(az,alt)=>{const rr=(90-Math.max(alt,0))/90*R;return[cx-rr*Math.sin(az*rad),cy-rr*Math.cos(az*rad)]};for(const o of S.objs){const pos=objectPosition(o);if(!pos||pos.alt<0)continue;const kind=objectKind(o);if(kind!=='other'&&!O[kind])continue;const [ox,oy]=xy(pos.az,pos.alt),d=Math.hypot(x-ox,y-oy);if(d<dist){dist=d;best=o}}if(best){const target=best.spec||best.name||best.label;if(target){setTarget(String(target));toast(`Target: ${best.label||best.name||target}`)}}});
|
||||
|
||||
function pip(){
|
||||
const c=$('pip');c.style.display='block';const g=c.getContext('2d'),R=185,cx=210,cy=210;g.clearRect(0,0,420,420);
|
||||
g.strokeStyle='#283246';g.fillStyle='#8491a6';g.font='20px system-ui';g.textAlign='center';[R,R*2/3,R/3].forEach((r)=>{g.beginPath();g.arc(cx,cy,r,0,Math.PI*2);g.stroke()});
|
||||
[['N',0],['E',90],['S',180],['W',270]].forEach(([l,a])=>g.fillText(l,cx-(R+14)*Math.sin(a*rad),cy-(R+14)*Math.cos(a*rad)+6));
|
||||
const xy=(az,alt)=>{const r=(90-Math.max(alt,0))/90*R;return[cx-r*Math.sin(az*rad),cy-r*Math.cos(az*rad)]};g.font='13px system-ui';
|
||||
for(const o of S.objs){const pos=objectPosition(o);if(!pos||pos.alt<0)continue;const kind=objectKind(o);if(kind!=='other'&&!O[kind])continue;const[x,y]=xy(pos.az,pos.alt),t=o.spec===S.target,label=String(o.label||o.name||o.spec||'');g.fillStyle=t||kind==='dso'?'#ffc247':kind==='terrestrial'?'#8fb4ff':'#cfd8f5';g.beginPath();if(kind==='dso')g.rect(x-3,y-3,6,6);else g.arc(x,y,t?6:kind==='stars'?2:4,0,Math.PI*2);g.fill();if(O.labels&&(t||kind!=='stars'))g.fillText(label.split(' ')[0],x+8,y-8)}
|
||||
if(S.pt){const[x,y]=xy(S.pt.az,S.pt.alt);g.strokeStyle='#ff5968';g.lineWidth=2;g.beginPath();g.arc(x,y,10,0,Math.PI*2);g.moveTo(x-16,y);g.lineTo(x+16,y);g.moveTo(x,y-16);g.lineTo(x,y+16);g.stroke();g.lineWidth=1}}
|
||||
|
||||
// ---------- recommendations / objects ----------
|
||||
async function loadRecs(){try{const r=await j('/api/sky/recommend?limit=8');
|
||||
$('recs').innerHTML=r.recommendations.length?'<tr class="mut"><th>Target</th><th>Alt</th><th>Mag</th><th>Why</th></tr>'+r.recommendations.map((x)=>`<tr class="rec ${x.spec===S.target?'sel':''}" data-spec="${x.spec}"><td>${x.label}<div class="mut">${x.type}</div></td><td>${x.alt}°</td><td>${x.mag??'–'}</td><td class="mut">${x.why.join(', ')}</td></tr>`).join(''):'<tr><td class="mut">Nothing above the horizon right now.</td></tr>'}
|
||||
catch(e){$('recs').innerHTML=`<tr><td class="bad">${e.message}</td></tr>`}}
|
||||
$('recs').onclick=(e)=>{const tr=e.target.closest('tr.rec');if(tr)setTarget(tr.dataset.spec)};
|
||||
const loadObjs=async()=>{try{const r=await j('/api/sky/objects');S.objs=Array.isArray(r.objects)?r.objects:[];S.constellationLines=Array.isArray(r.constellationLines)?r.constellationLines:[];draw();pip()}catch(e){S.objs=[];S.constellationLines=[];draw();pip()}};
|
||||
capUI();loadObjs();loadRecs();refreshInfo();tick();
|
||||
setInterval(tick,1000);setInterval(loadObjs,5000);setInterval(loadRecs,60000);setInterval(refreshInfo,15000);
|
||||
</script></body></html>
|
||||
+157
-28
@@ -1,15 +1,20 @@
|
||||
import { createServer } from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
import fsp from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
import { TelemetryService } from './services/telemetry.mjs';
|
||||
import { CameraService } from './services/camera.mjs';
|
||||
import { CelestialService } from './services/celestial.mjs';
|
||||
import { SkyService } from './services/sky.mjs';
|
||||
|
||||
const ROOT = path.dirname(fileURLToPath(import.meta.url));
|
||||
const DATA_DIR = path.resolve(process.env.DATA_DIR || path.join(ROOT, 'data'));
|
||||
const PORT = Number(process.env.PORT || 8080);
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
// UI lives in the project root. Override with UI_FILE if you keep it elsewhere (e.g. ui/index.html).
|
||||
const UI_FILE = path.resolve(process.env.UI_FILE || path.join(ROOT, 'index.html'));
|
||||
|
||||
const log = Object.fromEntries(['debug', 'info', 'warn', 'error'].map((level) => [
|
||||
level,
|
||||
@@ -43,9 +48,9 @@ const cameraConfig = {
|
||||
vidBin: process.env.RPICAM_VID || 'rpicam-vid',
|
||||
stillBin: process.env.RPICAM_STILL || 'rpicam-still',
|
||||
ffmpegBin: process.env.FFMPEG || 'ffmpeg',
|
||||
width: Number(process.env.W || 1280),
|
||||
width: Number(process.env.W || 720),
|
||||
height: Number(process.env.H || 720),
|
||||
fps: Number(process.env.FPS || 30),
|
||||
fps: Number(process.env.FPS || 60),
|
||||
streamQuality: 85,
|
||||
streamMaxShutterUs: 1_000_000,
|
||||
streamIdleStopMs: 5000,
|
||||
@@ -81,6 +86,7 @@ if (process.env.DECLINATION_DEG !== undefined) {
|
||||
|
||||
const telemetry = new TelemetryService(telemetryConfig, { log });
|
||||
const celestial = new CelestialService(celestialConfig, { telemetry, log, dataDir: DATA_DIR });
|
||||
const sky = new SkyService({ celestial, telemetry });
|
||||
const camera = new CameraService(cameraConfig, {
|
||||
log,
|
||||
dataDir: DATA_DIR,
|
||||
@@ -106,6 +112,38 @@ function sendJson(res, status, data) {
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.js': 'text/javascript; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.png': 'image/png',
|
||||
'.mp4': 'video/mp4',
|
||||
'.h264': 'video/h264',
|
||||
'.dng': 'image/x-adobe-dng',
|
||||
};
|
||||
|
||||
async function serveFile(res, file, cache = 'no-cache') {
|
||||
let st;
|
||||
try {
|
||||
st = await fsp.stat(file);
|
||||
if (!st.isFile()) throw new Error('not a file');
|
||||
} catch {
|
||||
return sendJson(res, 404, { error: 'not_found' });
|
||||
}
|
||||
|
||||
res.writeHead(200, {
|
||||
'Content-Type': MIME[path.extname(file).toLowerCase()] || 'application/octet-stream',
|
||||
'Content-Length': st.size,
|
||||
'Cache-Control': cache,
|
||||
});
|
||||
fs.createReadStream(file)
|
||||
.on('error', (err) => res.destroy(err))
|
||||
.pipe(res);
|
||||
}
|
||||
|
||||
function readJson(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
@@ -134,8 +172,35 @@ function readJson(req) {
|
||||
|
||||
async function handle(req, res) {
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
const is = (method, pathname) => req.method === method && url.pathname === pathname;
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/') {
|
||||
// ───────────── UI + static ─────────────
|
||||
|
||||
if (is('GET', '/')) {
|
||||
res.writeHead(302, { Location: '/index.html', 'Cache-Control': 'no-store' });
|
||||
return res.end();
|
||||
}
|
||||
|
||||
if (is('GET', '/index.html')) {
|
||||
return serveFile(res, UI_FILE);
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname.startsWith('/media/')) {
|
||||
let rel;
|
||||
try {
|
||||
rel = decodeURIComponent(url.pathname.slice('/media/'.length));
|
||||
} catch {
|
||||
return sendJson(res, 400, { error: 'bad_path' });
|
||||
}
|
||||
const base = path.resolve(camera.mediaRoot);
|
||||
const file = path.resolve(base, rel);
|
||||
if (!file.startsWith(base + path.sep)) return sendJson(res, 403, { error: 'forbidden' });
|
||||
return serveFile(res, file, 'public, max-age=3600');
|
||||
}
|
||||
|
||||
// ───────────── API index ─────────────
|
||||
|
||||
if (is('GET', '/api')) {
|
||||
return sendJson(res, 200, {
|
||||
name: 'MRS',
|
||||
status: 'online',
|
||||
@@ -144,34 +209,29 @@ async function handle(req, res) {
|
||||
'/api/status',
|
||||
'/api/telemetry/battery',
|
||||
'/api/camera/settings',
|
||||
'/api/camera/files',
|
||||
'/api/celestial/predict?target=',
|
||||
'/api/celestial/delta?target=',
|
||||
'/api/celestial/targets',
|
||||
'/api/celestial/alignment',
|
||||
'/api/sky/info?target=',
|
||||
'/api/sky/recommend',
|
||||
'/api/sky/objects',
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/stream') {
|
||||
// ───────────── stream / telemetry ─────────────
|
||||
|
||||
if (is('GET', '/stream')) {
|
||||
return camera.addStreamClient(req, res);
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/telemetry/battery') {
|
||||
if (is('GET', '/api/telemetry/battery')) {
|
||||
return sendJson(res, 200, { battery: telemetry.getBattery() });
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/camera/settings') {
|
||||
return sendJson(res, 200, { settings: camera.getSettings() });
|
||||
}
|
||||
|
||||
if (req.method === 'PATCH' && url.pathname === '/api/camera/settings') {
|
||||
const body = await readJson(req);
|
||||
const settings = await camera.applySettings(body);
|
||||
return sendJson(res, 200, { ok: true, settings });
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/camera/settings/reset') {
|
||||
const settings = await camera.resetSettings();
|
||||
return sendJson(res, 200, { ok: true, settings });
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/status') {
|
||||
if (is('GET', '/api/status')) {
|
||||
let celestialStatus;
|
||||
|
||||
try {
|
||||
@@ -189,7 +249,53 @@ async function handle(req, res) {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/celestial/predict') {
|
||||
// ───────────── camera settings ─────────────
|
||||
|
||||
if (is('GET', '/api/camera/settings')) {
|
||||
return sendJson(res, 200, { settings: camera.getSettings() });
|
||||
}
|
||||
|
||||
if (is('PATCH', '/api/camera/settings')) {
|
||||
const body = await readJson(req);
|
||||
const settings = await camera.applySettings(body);
|
||||
return sendJson(res, 200, { ok: true, settings });
|
||||
}
|
||||
|
||||
if (is('POST', '/api/camera/settings/reset')) {
|
||||
const settings = await camera.resetSettings();
|
||||
return sendJson(res, 200, { ok: true, settings });
|
||||
}
|
||||
|
||||
// ───────────── camera capture jobs ─────────────
|
||||
|
||||
if (is('POST', '/api/camera/photo')) {
|
||||
return sendJson(res, 200, await camera.takePhoto(await readJson(req)));
|
||||
}
|
||||
|
||||
if (is('POST', '/api/camera/video/start')) {
|
||||
return sendJson(res, 200, { ok: true, job: await camera.startVideo(await readJson(req)) });
|
||||
}
|
||||
|
||||
if (is('POST', '/api/camera/video/stop')) {
|
||||
return sendJson(res, 200, await camera.stopVideo());
|
||||
}
|
||||
|
||||
if (is('POST', '/api/camera/timelapse/start')) {
|
||||
return sendJson(res, 200, { ok: true, job: await camera.startTimelapse(await readJson(req)) });
|
||||
}
|
||||
|
||||
if (is('POST', '/api/camera/timelapse/stop')) {
|
||||
return sendJson(res, 200, await camera.stopTimelapse());
|
||||
}
|
||||
|
||||
if (is('GET', '/api/camera/files')) {
|
||||
const limit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit') || 50)));
|
||||
return sendJson(res, 200, await camera.listFiles({ limit }));
|
||||
}
|
||||
|
||||
// ───────────── celestial ─────────────
|
||||
|
||||
if (is('GET', '/api/celestial/predict')) {
|
||||
const target = url.searchParams.get('target');
|
||||
if (!target) {
|
||||
return sendJson(res, 400, {
|
||||
@@ -200,7 +306,7 @@ async function handle(req, res) {
|
||||
return sendJson(res, 200, celestial.predict(target));
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/celestial/delta') {
|
||||
if (is('GET', '/api/celestial/delta')) {
|
||||
const target = url.searchParams.get('target');
|
||||
if (!target) {
|
||||
return sendJson(res, 400, {
|
||||
@@ -211,18 +317,18 @@ async function handle(req, res) {
|
||||
return sendJson(res, 200, celestial.delta(target));
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/celestial/targets') {
|
||||
if (is('GET', '/api/celestial/targets')) {
|
||||
const kind = url.searchParams.get('kind') || undefined;
|
||||
const q = url.searchParams.get('q') || undefined;
|
||||
const limit = Math.min(500, Math.max(1, Number(url.searchParams.get('limit') || 100)));
|
||||
return sendJson(res, 200, { targets: celestial.listTargets({ kind, q, limit }) });
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/api/celestial/alignment') {
|
||||
if (is('GET', '/api/celestial/alignment')) {
|
||||
return sendJson(res, 200, celestial.getAlignment());
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/celestial/zero') {
|
||||
if (is('POST', '/api/celestial/zero')) {
|
||||
const body = await readJson(req);
|
||||
if (!body.target) {
|
||||
return sendJson(res, 400, {
|
||||
@@ -233,15 +339,38 @@ async function handle(req, res) {
|
||||
return sendJson(res, 200, await celestial.zero(body.target));
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE' && url.pathname === '/api/celestial/zero') {
|
||||
if (is('DELETE', '/api/celestial/zero')) {
|
||||
return sendJson(res, 200, await celestial.clearZero());
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && url.pathname === '/api/celestial/tle/refresh') {
|
||||
if (is('POST', '/api/celestial/tle/refresh')) {
|
||||
const body = await readJson(req);
|
||||
return sendJson(res, 200, { results: await celestial.refreshTLEs({ force: body.force !== false }) });
|
||||
}
|
||||
|
||||
// ───────────── sky (info / recommendations / overlay) ─────────────
|
||||
|
||||
if (is('GET', '/api/sky/info')) {
|
||||
const target = url.searchParams.get('target');
|
||||
if (!target) {
|
||||
return sendJson(res, 400, {
|
||||
error: 'missing_target',
|
||||
message: 'Pass ?target=moon, saturn, vega, iss, radec:5.9,7.4, etc.',
|
||||
});
|
||||
}
|
||||
return sendJson(res, 200, sky.info(target));
|
||||
}
|
||||
|
||||
if (is('GET', '/api/sky/recommend')) {
|
||||
const limit = Math.min(50, Math.max(1, Number(url.searchParams.get('limit') || 8)));
|
||||
const minAlt = Number(url.searchParams.get('minAlt') || 15);
|
||||
return sendJson(res, 200, { recommendations: sky.recommend({ limit, minAlt }) });
|
||||
}
|
||||
|
||||
if (is('GET', '/api/sky/objects')) {
|
||||
return sendJson(res, 200, sky.objects());
|
||||
}
|
||||
|
||||
return sendJson(res, 404, { error: 'not_found', path: url.pathname });
|
||||
}
|
||||
|
||||
|
||||
+28
-12
@@ -1,4 +1,3 @@
|
||||
|
||||
// Camera service (rpicam-apps wrapper)
|
||||
//
|
||||
// libcamera lets only ONE process own the camera, so everything here goes through a single
|
||||
@@ -401,11 +400,21 @@ export class CameraService extends EventEmitter {
|
||||
}, this.#cfg.streamIdleStopMs);
|
||||
}
|
||||
|
||||
// Resolve exposure and frame timing once so preview and recording don't silently
|
||||
// choose different shutter speeds or incompatible frame rates.
|
||||
#cameraPlan(settings, requestedFps) {
|
||||
const shutterUs = settings.shutterUs > 0 ? settings.shutterUs : 0;
|
||||
const fps = shutterUs > 0
|
||||
? Math.min(Number(requestedFps), 1_000_000 / shutterUs)
|
||||
: Number(requestedFps);
|
||||
|
||||
return { settings, shutterUs, fps: Math.max(0.001, fps) };
|
||||
}
|
||||
|
||||
#streamArgs() {
|
||||
const c = this.#cfg;
|
||||
const s = this.#settings;
|
||||
const shutter = s.shutterUs > 0 ? Math.min(s.shutterUs, c.streamMaxShutterUs) : 0;
|
||||
const fps = shutter > 0 ? Math.min(c.fps, 1_000_000 / shutter) : c.fps;
|
||||
const plan = this.#cameraPlan(this.#settings, c.fps);
|
||||
const { settings, shutterUs, fps } = plan;
|
||||
|
||||
return {
|
||||
fps,
|
||||
@@ -415,8 +424,8 @@ export class CameraService extends EventEmitter {
|
||||
'--height', String(c.height),
|
||||
'--framerate', fps.toFixed(3),
|
||||
'--codec', 'mjpeg',
|
||||
'-q', String(s.quality),
|
||||
...this.#controlArgs(s, { shutterUs: shutter }),
|
||||
'-q', String(settings.quality),
|
||||
...this.#controlArgs(settings, { shutterUs }),
|
||||
'-o', '-',
|
||||
],
|
||||
};
|
||||
@@ -424,7 +433,7 @@ export class CameraService extends EventEmitter {
|
||||
|
||||
#startStream() {
|
||||
const { fps, args } = this.#streamArgs();
|
||||
this.#log.info(`starting live view ${this.#cfg.width}x${this.#cfg.height} @ ${fps.toFixed(1)} fps`);
|
||||
this.#log.info(`starting live view ${this.#cfg.width}x${this.#cfg.height} @ ${fps.toFixed(3)} fps`);
|
||||
|
||||
const proc = spawn(this.#cfg.vidBin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
||||
const stream = { proc, fps, expected: false, startedAt: Date.now(), frames: 0, exited: null };
|
||||
@@ -584,7 +593,8 @@ export class CameraService extends EventEmitter {
|
||||
|
||||
#stillArgs(file, settings, { raw = false } = {}) {
|
||||
const c = this.#cfg;
|
||||
const manual = settings.shutterUs > 0 && settings.gain > 0;
|
||||
const { shutterUs } = this.#cameraPlan(settings, 1);
|
||||
const manual = shutterUs > 0 && settings.gain > 0;
|
||||
|
||||
return [
|
||||
'-n',
|
||||
@@ -681,7 +691,9 @@ export class CameraService extends EventEmitter {
|
||||
|
||||
const w = width || c.videoWidth;
|
||||
const h = height || c.videoHeight;
|
||||
const f = fps || c.videoFps;
|
||||
const requestedFps = Number(fps || c.videoFps);
|
||||
const plan = this.#cameraPlan(settings, requestedFps);
|
||||
const f = plan.fps;
|
||||
const base = `video_${stamp()}`;
|
||||
const rawFile = path.join(this.#dirs.videos, `${base}.h264`);
|
||||
|
||||
@@ -696,7 +708,7 @@ export class CameraService extends EventEmitter {
|
||||
'--framerate', String(f),
|
||||
'--codec', 'h264',
|
||||
'--inline',
|
||||
...this.#controlArgs(settings),
|
||||
...this.#controlArgs(plan.settings, { shutterUs: plan.shutterUs }),
|
||||
'-o', rawFile,
|
||||
];
|
||||
|
||||
@@ -750,7 +762,11 @@ export class CameraService extends EventEmitter {
|
||||
proc.once('close', (code, signal) => finish(code, signal));
|
||||
});
|
||||
|
||||
this.#log.info(`video recording started (${w}x${h} @ ${f} fps${durationSec ? `, ${durationSec}s` : ''})`);
|
||||
this.#log.info(
|
||||
`video recording started (${w}x${h} @ ${f.toFixed(3)} fps` +
|
||||
`${f < requestedFps ? `, limited from ${requestedFps} fps by ${plan.shutterUs}µs shutter` : ''}` +
|
||||
`${durationSec ? `, ${durationSec}s` : ''})`,
|
||||
);
|
||||
return this.#jobSummary();
|
||||
}
|
||||
|
||||
@@ -1077,4 +1093,4 @@ export class CameraService extends EventEmitter {
|
||||
dir: `/media/timelapse/${job.id}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
// Sky service: target info, "what should I look at" recommendations, and overlay objects.
|
||||
// Positions come from CelestialService.predict() (astronomy-engine, fully offline for Sun/Moon/planets/stars).
|
||||
import * as Astronomy from 'astronomy-engine';
|
||||
import { STARS } from '../lib/stars.mjs';
|
||||
import { angularSeparation } from '../lib/geo.mjs';
|
||||
|
||||
// Bundled deep-sky showpieces (J2000 RA hours, Dec deg, visual magnitude). Swap for OpenNGC later.
|
||||
const DSO = [
|
||||
['m31', 'M31 Andromeda Galaxy', 'Galaxy', 0.712, 41.27, 3.4],
|
||||
['m42', 'M42 Orion Nebula', 'Nebula', 5.588, -5.39, 4.0],
|
||||
['m45', 'M45 Pleiades', 'Open Cluster', 3.79, 24.12, 1.6],
|
||||
['m44', 'M44 Beehive', 'Open Cluster', 8.667, 19.67, 3.7],
|
||||
['m13', 'M13 Hercules Cluster', 'Globular', 16.695, 36.46, 5.8],
|
||||
['m8', 'M8 Lagoon Nebula', 'Nebula', 18.063, -24.38, 6.0],
|
||||
['m27', 'M27 Dumbbell Nebula', 'Planetary Nebula', 19.994, 22.72, 7.4],
|
||||
['m57', 'M57 Ring Nebula', 'Planetary Nebula', 18.893, 33.03, 8.8],
|
||||
['m81', 'M81 Bode\'s Galaxy', 'Galaxy', 9.926, 69.07, 6.9],
|
||||
['m51', 'M51 Whirlpool Galaxy', 'Galaxy', 13.498, 47.2, 8.4],
|
||||
].map(([id, label, type, ra, dec, mag]) => ({ id, label, type, ra, dec, mag, spec: `radec:${ra},${dec}` }));
|
||||
|
||||
const PLANETS = ['mercury', 'venus', 'mars', 'jupiter', 'saturn', 'uranus', 'neptune'];
|
||||
const OUTER = ['Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune'];
|
||||
const r = (v, d = 1) => (v == null ? v : Math.round(v * 10 ** d) / 10 ** d);
|
||||
const iso = (t) => t?.date?.toISOString?.() ?? null;
|
||||
const cap = (s) => s[0].toUpperCase() + s.slice(1);
|
||||
|
||||
export class SkyService {
|
||||
#c;
|
||||
#t;
|
||||
|
||||
constructor({ celestial, telemetry }) {
|
||||
this.#c = celestial;
|
||||
this.#t = telemetry;
|
||||
}
|
||||
|
||||
#candidates() {
|
||||
return [
|
||||
{ id: 'body:moon', label: 'Moon', type: 'Moon', spec: 'moon' },
|
||||
...PLANETS.map((p) => ({ id: `body:${p}`, label: cap(p), type: 'Planet', spec: `body:${p}` })),
|
||||
...Object.entries(STARS).map(([k, s]) => ({ id: `star:${k}`, label: s.label, type: 'Star', mag: s.mag ?? 2, spec: `star:${k}` })),
|
||||
...DSO.map((d) => ({ ...d, id: `dso:${d.id}` })),
|
||||
];
|
||||
}
|
||||
|
||||
/** Detail for one target: position + (for solar-system bodies) phase, magnitude, elongation, opposition, rise/transit/set. */
|
||||
info(spec) {
|
||||
const date = this.#t.now();
|
||||
const p = this.#c.predict(spec, { date });
|
||||
const out = { ...p };
|
||||
if (p.target.kind !== 'body') return out;
|
||||
|
||||
const name = p.target.label;
|
||||
const o = new Astronomy.Observer(p.observer.lat, p.observer.lon, p.observer.altM);
|
||||
const ill = Astronomy.Illumination(name, date);
|
||||
out.magnitude = r(ill.mag);
|
||||
out.phasePct = r(ill.phase_fraction * 100, 0);
|
||||
out.distanceKm = Math.round(p.distanceAu * 149_597_870.7);
|
||||
out.elongationDeg = name === 'Sun' ? 0 : r(Astronomy.AngleFromSun(name, date));
|
||||
out.events = {
|
||||
rise: iso(Astronomy.SearchRiseSet(name, o, +1, date, 2)),
|
||||
transit: iso(Astronomy.SearchHourAngle(name, o, 0, date).time),
|
||||
set: iso(Astronomy.SearchRiseSet(name, o, -1, date, 2)),
|
||||
};
|
||||
if (OUTER.includes(name)) {
|
||||
out.oppositionPct = r((out.elongationDeg / 180) * 100, 0); // 100% = exactly at opposition
|
||||
out.nextOpposition = iso(Astronomy.SearchRelativeLongitude(name, 180, date));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Ranked list of what's worth observing right now. */
|
||||
recommend({ limit = 8, minAlt = 15 } = {}) {
|
||||
const date = this.#t.now();
|
||||
const moon = this.#c.predict('moon', { date });
|
||||
const moonIllum = (1 - Math.cos((Astronomy.MoonPhase(date) * Math.PI) / 180)) / 2;
|
||||
const rows = [];
|
||||
|
||||
for (const c of this.#candidates()) {
|
||||
let p;
|
||||
try {
|
||||
p = this.#c.predict(c.spec, { date });
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (p.alt < minAlt) continue;
|
||||
|
||||
const row = { id: c.id, spec: c.spec, label: c.label, type: c.type, az: p.az, alt: p.alt, why: [] };
|
||||
let mag = c.mag;
|
||||
let score = (Math.min(p.alt, 70) / 70) * 40;
|
||||
|
||||
if (c.type === 'Planet' || c.type === 'Moon') {
|
||||
const ill = Astronomy.Illumination(c.label, date);
|
||||
mag = ill.mag;
|
||||
row.phasePct = r(ill.phase_fraction * 100, 0);
|
||||
const el = Astronomy.AngleFromSun(c.label, date);
|
||||
if (OUTER.includes(c.label)) {
|
||||
row.oppositionPct = r((el / 180) * 100, 0);
|
||||
if (el > 150) { score += 15; row.why.push('near opposition'); }
|
||||
}
|
||||
if (el < 20 && c.type === 'Planet') { score -= 30; row.why.push('close to the Sun'); }
|
||||
if (c.type === 'Planet') score += 10;
|
||||
}
|
||||
const moonSep = angularSeparation(p.az, p.alt, moon.az, moon.alt);
|
||||
if (c.type !== 'Moon' && mag > 4 && moonSep < 40) {
|
||||
score -= (40 - moonSep) * 0.6 * moonIllum;
|
||||
row.why.push('washed out by Moon');
|
||||
}
|
||||
score += Math.max(0, 10 - (mag ?? 10)) * 3;
|
||||
if (p.alt > 50) row.why.push('high in sky');
|
||||
|
||||
row.mag = r(mag);
|
||||
row.moonSepDeg = r(moonSep, 0);
|
||||
row.score = r(score, 0);
|
||||
rows.push(row);
|
||||
}
|
||||
return rows.sort((a, b) => b.score - a.score).slice(0, limit);
|
||||
}
|
||||
|
||||
/** Everything above (roughly) the horizon, for the sky-map / live overlay. */
|
||||
objects() {
|
||||
const date = this.#t.now();
|
||||
const out = [];
|
||||
for (const c of this.#candidates()) {
|
||||
try {
|
||||
const p = this.#c.predict(c.spec, { date });
|
||||
if (p.alt > -5) out.push({ id: c.id, spec: c.spec, label: c.label, type: c.type, mag: c.mag ?? null, az: p.az, alt: p.alt });
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
return { time: date.toISOString(), objects: out };
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user