-
Notifications
You must be signed in to change notification settings - Fork 3
/
print_transits.cgi
executable file
·3398 lines (2828 loc) · 120 KB
/
print_transits.cgi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/perl
# Code to read in a file of targets for eclipse/transit observations,
# find upcoming events, sort them according to date, and print the
# results, either as HTML, or in CSV format to be read
# into Google Calendar or another calendar program. Input parameters
# provided by transits.cgi, which calls this script.
# Copyright 2012-2023 Eric Jensen, [email protected].
#
# This file is part of the Tapir package, a set of (primarily)
# web-based tools for planning astronomical observations. For more
# information, see the README.txt file or
# https://astro.swarthmore.edu/~jensen/tapir.html .
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program, in the file COPYING.txt. If not, see
# <https://www.gnu.org/licenses/>.
# These next settings are turned on to have the strictest possible
# error checking and warnings, to help find obscure bugs. If they are
# causing problems, any of them can be commented out.
# Require declaration of all variables:
use strict;
# Warn about suspect constructions, uninitalized variables, etc.
# Equivalent to the -w command-line switch.
use warnings;
# Cause fatal errors to print to browser rather than (in addition to?)
# webserver log file:
use CGI::Carp qw(fatalsToBrowser);
# Some observatory names have non-ASCII characters:
use utf8;
use Astro::Coords;
use Astro::Telescope;
# Constants for degrees to radians and vice versa, and angular
# separation routine:
use Astro::PAL qw( DD2R DR2D palDsep );
# Note - code at the end of this file adds a couple of new methods to
# DateTime and DateTime::Duration that aren't in the core module.
use DateTime;
use DateTime::Duration;
use DateTime::Set;
use DateTime::Format::Epoch::JD;
use HTML::Template::Expr;
use CGI qw/ -utf8 /;
use CGI::Cookie;
use URI::Escape;
use HTML::Entities;
use Switch;
use List::Util qw (min max);
use Text::CSV qw( csv );
use Parallel::ForkManager;
# We should be getting UTF-8 data from our target list, so make sure
# we output in the same format. It's not clear whether this makes a
# difference vs. just setting the character set in the HTTP header,
# but it doesn't hurt.
binmode(STDOUT, ":utf8");
############# Variables for local configuration ##############
# Put things here that are likely to need to be changed in order to
# use this code for a different purpose:
# Maximum number of events to output; set to avoid searches
# that would tie up server for a long time:
my $max_eclipses_to_print = 1000;
# Template for the HTML output page:
my $template_filename = 'target_table.tmpl';
# A template for CSV output; this may supersede the above setting
# based on user input below.
my $csv_template_filename = 'csv_text.tmpl';
# File containing target info; include path as needed.
# This is the default value, subject to change below based on other
# flags.
my $target_file = 'transit_targets.csv';
# Parameters for setting cookies stored in the user's
# browser to remember input parameters:
my $cookie_domain = '.astro.swarthmore.edu';
my $cookie_path = '';
my $cookie_expires = '+1M';
# Contact info provided in the fatal_error subroutine:
my $script_contact_person = 'Eric Jensen, [email protected]';
# Look at the subroutine 'parse_target_line' toward the end of the
# file to see the default assumed format for the input target file,
# and to change how that line is parsed into variables if necessary.
# You should also edit the subroutines 'finding_chart_page' and
# 'target_info_page' at the end of this file to either (a) return
# valid URLs for your targets or (b) return undef.
######## End variables for local configuration ###############
# Switch to turn on some very rudimentary debugging output:
my $debug = 0;
# Very rudimentary timing of the script:
my $script_start_time = time;
# Define the CGI object that will both allow us to fetch the input
# variables to the script and also to print the HTML output:
my $q = CGI->new();
# First, get the necessary input parameters for calculating the
# transit visibility; these come from a separate page that passes them
# into this script.
# Observatory latitude and longitude; give in degrees, with positive
# for north latitude and east longitude; use negative for south
# latitude or west longitude.
# There are two different possible ways that this info can be
# specified. For the pre-defined observatories, it is passed as a
# semicolon-separated string that has latitude, longitude, and
# timezone all in one field. Otherwise, those three quantities can be
# specified separately in individual fields.
my $observatory_string = $q->param("observatory_string");
my $flag_for_manual_entry = 'Specified_Lat_Long';
if ((not defined $observatory_string) or ($observatory_string eq "")) {
$observatory_string = $flag_for_manual_entry;
}
my ($observatory_latitude, $observatory_longitude,
$temporary_timezone, $observatory_timezone,
$observatory_name, $observatory_shortname);
# Check to see if the entered string contains the text that indicates
# we should ignore it and use individual fields instead, or if we
# should try to parse it.
if ($observatory_string !~ /$flag_for_manual_entry/) {
($observatory_latitude, $observatory_longitude,
$temporary_timezone, $observatory_name, $observatory_shortname)
= split(/;/, $observatory_string);
} else {
$observatory_longitude = $q->param("observatory_longitude");
$observatory_latitude = $q->param("observatory_latitude");
$temporary_timezone = $q->param("timezone");
$observatory_name = "Other Site";
$observatory_shortname = "Other Site";
}
# The timezone string gets used in an 'eval' statement by
# DateTime::Timezone at some point, so we need to untaint it here by
# checking it against a regular expression. We have to allow a '/'
# here, even though it is a path separator, because it is a legitimate
# part of some timezone names.
if ($temporary_timezone =~ m%^\s*([_/+\-0-9A-Za-z]+)$%) {
$observatory_timezone = encode_entities($1);
} else {
my $err_timezone = encode_entities($temporary_timezone);
die "Unrecognized timezone: [$err_timezone]\n";
}
# Make sure latitude and longitude only have valid chars:
$observatory_longitude = num_only($observatory_longitude);
$observatory_latitude = num_only($observatory_latitude);
# And likewise for names:
$observatory_name = encode_entities($observatory_name);
$observatory_shortname = encode_entities($observatory_shortname);
# Check to see if they set the parameter to use UTC no matter what.
# We keep $observatory_timezone set to the local timezone, but
# use this boolean to check what to use for output for main display.
my $use_utc = num_only($q->param("use_utc"));
if ((not defined $use_utc) or ($use_utc =~ /^\s*$/)) {
$use_utc = 0;
}
# Desired time windows for data:
# Start date:
my $start_date_string = encode_entities($q->param("start_date"));
if ((not defined $start_date_string) or ($start_date_string =~ /^\s*$/)) {
$start_date_string = 'today';
}
# Days in the future to print (including start date):
my $days_to_print = num_only($q->param("days_to_print"));
if ((not defined $days_to_print) or ($days_to_print =~ /^\s*$/)) {
$days_to_print = 1;
}
# Days in the past (based from start date) to print:
my $days_in_past = num_only($q->param("days_in_past"));
# If they didn't specify a backward-looking window, then only show
# future eclipses:
if ((not defined $days_in_past) or ($days_in_past =~ /^\s*$/)) {
$days_in_past = 0;
}
# Minimum start/end elevation to show; default to 0:
my $minimum_start_elevation = num_only($q->param("minimum_start_elevation"));
if ((not defined $minimum_start_elevation)
or ($minimum_start_elevation =~ /^\s*$/)) {
$minimum_start_elevation = 0;
}
my $minimum_end_elevation = num_only($q->param("minimum_end_elevation"));
if ((not defined $minimum_end_elevation)
or ($minimum_end_elevation =~ /^\s*$/)) {
$minimum_end_elevation = 0;
}
# Min/max hour angle to show; default to +/- 12H:
my $minimum_ha = num_only($q->param("minimum_ha"));
if ((not defined $minimum_ha)
or ($minimum_ha =~ /^\s*$/)) {
$minimum_ha = -12;
}
my $maximum_ha = num_only($q->param("maximum_ha"));
if ((not defined $maximum_ha)
or ($maximum_ha =~ /^\s*$/)) {
$maximum_ha = 12;
}
my $baseline_hrs = num_only($q->param("baseline_hrs"));
if ((not defined $baseline_hrs)
or ($baseline_hrs =~ /^\s*$/)) {
$baseline_hrs = 0;
}
my $show_unc = num_only($q->param("show_unc"));
if ((not defined $show_unc)
or ($show_unc =~ /^\s*$/)) {
$show_unc = 0;
}
# Fall back on 'or' behavior if there is it isn't defined,
# or if there is anything other than "and" or "or" in that flag:
my $and_vs_or = lc($q->param("and_vs_or"));
if ((not defined $and_vs_or)
or ($and_vs_or eq "") or ($and_vs_or !~ /^(and|or)$/)) {
$and_vs_or = "or";
}
# Special flag for observing from space; no elevation or day/night
# constraints on transit visibility:
my $observing_from_space = num_only($q->param("space"));
if ((not defined $observing_from_space)
or ($observing_from_space eq "")) {
$observing_from_space = 0;
}
# Minimum priority to show; default to zero:
my $minimum_priority = num_only($q->param("minimum_priority"));
if ((not defined $minimum_priority) or ($minimum_priority eq "")) {
$minimum_priority = 0;
}
# Minimum depth (in ppt) to show; default to zero:
my $minimum_depth = num_only($q->param("minimum_depth"));
if ((not defined $minimum_depth) or ($minimum_depth =~ /^\s*$/)) {
$minimum_depth = 0;
}
# Maximum (faintest) V mag to show:
my $maximum_V_mag = num_only($q->param("maximum_V_mag"));
if ((not defined $maximum_V_mag) or ($maximum_V_mag =~ /^\s*$/)) {
$maximum_V_mag = 30;
}
# Maximum airmass for airmass plots:
my $max_airmass = num_only($q->param("max_airmass"));
if ((not defined $max_airmass) or ($max_airmass =~ /^\s*$/)) {
$max_airmass = 2.4;
}
# Whether to show detector outline:
my $showFov = num_only($q->param("showFov"));
if ((not defined $showFov) or ($showFov =~ /^\s*$/)) {
$showFov = 0;
}
# Size / orientation for outline:
my $fovWidth = num_only($q->param("fovWidth"));
if ((not defined $fovWidth) or ($fovWidth =~ /^\s*$/)) {
$fovWidth = '';
}
my $fovHeight = num_only($q->param("fovHeight"));
if ((not defined $fovHeight) or ($fovHeight =~ /^\s*$/)) {
$fovHeight = '';
}
my $fovPA = num_only($q->param("fovPA"));
if ((not defined $fovPA) or ($fovPA =~ /^\s*$/)) {
$fovPA = '';
}
# Target name string to match (can be a regex):
my $target_string = encode_entities($q->param("target_string"));
if (not defined $target_string) {
$target_string = '';
} else {
# Eliminate the most obvious vulnerability here:
$target_string =~ s/script//ig;
# Strip leading and trailing whitespace on this string:
$target_string =~ s/^\s*(\S+)\s*$/$1/;
}
# Check to see if we are doing just a single object with manual
# ephemeris entry. If this is set to 1, it means that we
# will not read the target file, but will instead take the
# entered ephemeris for a single object and just use that.
my $single_object = num_only($q->param("single_object"));
if ((not defined $single_object) or ($single_object eq "")) {
$single_object = 0;
}
# The 'single_object' flag is overloaded to possibly indicate
# alternate target lists:
my $tess = 0;
my $exowatch = 0;
# Flag for doing TOIs instead of known planets:
if ($single_object == 2) {
$target_file = 'toi_targets.csv';
# $template_filename = 'target_table.tmpl'; # may change this
$tess = 1;
} elsif ($single_object == 3) {
$target_file = 'exoplanet_watch_targets.csv';
$template_filename = 'target_table_exowatch.tmpl';
$exowatch = 1;
}
# Whether to show the ephemeris data:
my $show_ephemeris = num_only($q->param("show_ephemeris"));
# How to define twilight (given value is altitude of Sun at division
# between day and night, e.g. -12 for nautical twilight).
my $twilight = num_only($q->param("twilight"));
if ((not defined $twilight) or ($twilight =~ /^\s*$/)) {
$twilight = -12;
}
# Desired orbital phase to calculate and plot. Zero is transit.
my $phase = num_only($q->param("phase"));
if ((not defined $phase) or ($phase =~ /^\s*$/)) {
$phase = 0;
}
my $par_ref;
# If some parameters are entered with a comma instead of decimal point
# (different localization), substitute so the number is handled
# correctly:
foreach $par_ref (\$observatory_latitude, \$observatory_longitude,
\$twilight, \$max_airmass, \$maximum_V_mag,
\$minimum_start_elevation, \$minimum_end_elevation,
\$minimum_ha, \$maximum_ha, \$days_to_print,
\$days_in_past, \$phase, \$minimum_depth,
\$minimum_priority) {
if ($$par_ref =~ /\d,\d/) {
$$par_ref =~ s/,/\./;
}
}
# Set up the value in radians of this angle (which is what the
# calculations actually require, as well as a label to use for the
# output:
my ($twilight_rad, $twilight_label);
switch ($twilight) {
case -1 {$twilight_rad = Astro::Coords::SUN_RISE_SET;
$twilight_label = "Sunrise/sunset"; }
case -6 {$twilight_rad = Astro::Coords::CIVIL_TWILIGHT;
$twilight_label = "Civil twilight"; }
case -12 {$twilight_rad = Astro::Coords::NAUT_TWILIGHT;
$twilight_label = "Nautical twilight"; }
case -18 {$twilight_rad = Astro::Coords::AST_TWILIGHT;
$twilight_label = "Astronomical twilight"; }
# Interpret other values as elevation in degrees:
else {$twilight_rad = DD2R * $twilight;
$twilight_label = sprintf("Sun elev. %0.1f°", $twilight);
# Make the minus sign look a little nicer:
$twilight_label =~ s/-(\d)/–$1/; }
}
# Whether the output will be printed as an HTML table; if this
# parameter is not set, then the output is printed in a
# comma-delimited form suitable for import into a calendar program,
# e.g. Google Calendar.
my $print_html = num_only($q->param("print_html"));
# If they don't pass the parameter at all, print HTML rather than CSV:
if (not defined $print_html) {
$print_html = 1;
}
if ($print_html == 2) {
# Signal for raw CSV output; change the template:
$template_filename = $csv_template_filename;
}
# As long as the user allows it, we set cookies for many of the input
# parameters; that way they are filled in with useful default values
# the next time the user visits the input form. (For example, the
# latitude/longitude and/or observatory are remembered across sessions.)
my $observatory_cookie = CGI::Cookie->
new(-name => 'observatory_string',
-value => "$observatory_string",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $utc_cookie = CGI::Cookie->
new(-name => 'Use_UTC',
-value => "$use_utc",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $unc_cookie = CGI::Cookie->
new(-name => 'Show_uncertainty',
-value => "$show_unc",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $and_cookie = CGI::Cookie->
new(-name => 'Use_AND',
-value => "$and_vs_or",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $latitude_cookie = CGI::Cookie->
new(-name => 'observatory_latitude',
-value => "$observatory_latitude",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $longitude_cookie = CGI::Cookie->
new(-name => 'observatory_longitude',
-value => "$observatory_longitude",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $timezone_cookie = CGI::Cookie->
new(-name => 'observatory_timezone',
-value => "$observatory_timezone",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $days_cookie = CGI::Cookie->
new(-name => 'days_to_print',
-value => "$days_to_print",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $days_in_past_cookie = CGI::Cookie->
new(-name => 'days_in_past',
-value => "$days_in_past",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $minimum_start_elevation_cookie = CGI::Cookie->
new(-name => 'minimum_start_elevation',
-value => "$minimum_start_elevation",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $minimum_end_elevation_cookie = CGI::Cookie->
new(-name => 'minimum_end_elevation',
-value => "$minimum_end_elevation",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $minimum_ha_cookie = CGI::Cookie->
new(-name => 'minimum_ha',
-value => "$minimum_ha",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $maximum_ha_cookie = CGI::Cookie->
new(-name => 'maximum_ha',
-value => "$maximum_ha",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $baseline_hrs_cookie = CGI::Cookie->
new(-name => 'baseline_hrs',
-value => "$baseline_hrs",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $minimum_priority_cookie = CGI::Cookie->
new(-name => 'minimum_priority',
-value => "$minimum_priority",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $minimum_depth_cookie = CGI::Cookie->
new(-name => 'minimum_depth',
-value => "$minimum_depth",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $twilight_cookie = CGI::Cookie->
new(-name => 'twilight',
-value => "$twilight",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $max_airmass_cookie = CGI::Cookie->
new(-name => 'max_airmass',
-value => "$max_airmass",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $show_fov_cookie = CGI::Cookie->
new(-name => 'showFov',
-value => "$showFov",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $fov_width_cookie = CGI::Cookie->
new(-name => 'fovWidth',
-value => "$fovWidth",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $fov_height_cookie = CGI::Cookie->
new(-name => 'fovHeight',
-value => "$fovHeight",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
my $fov_PA_cookie = CGI::Cookie->
new(-name => 'fovPA',
-value => "$fovPA",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
# Only store the V mag value if it's constraining, otherwise blank:
my $maximum_Vmag_cookie = CGI::Cookie->
new(-name => 'maximum_V_mag',
-value => $maximum_V_mag < 30 ? "$maximum_V_mag" : "",
-expires => $cookie_expires,
-domain => $cookie_domain,
-path => $cookie_path,
);
# Now set up objects that will let us calculate the times of sunrise
# and sunset, necessary for determining observability.
my $sun = new Astro::Coords(planet => "sun");
my $moon = new Astro::Coords(planet => "moon");
# Associate the observatory coordinates with this object by defining a
# telescope object for our coordinates. The library requires latitude
# and longitude in radians; the constant DD2R (decimal degrees to
# radians) is defined by Astro::PAL. We specify an altitude of 0,
# since we need to say something; presumably this could be specified
# on input if we wanted to be even more precise about rise and set
# times.
my $telescope = new Astro::Telescope(Name => "MyObservatory",
Long => $observatory_longitude*DD2R,
Lat => $observatory_latitude*DD2R,
Alt => 0,
);
$sun->telescope($telescope);
$moon->telescope($telescope);
# Calculate the set of sunrise and sunset times that we will use later
# for testing transit observability. We make a list of these times
# here, so that we can pass them in to the subroutine and re-use them
# for each subsequent target.
# To do this, we need to find the date around which we are basing the
# calculation.
# If they specify 'today', we just do 24 hours from the present time.
# But if they pick another date, we want to do noon to noon *in the
# timezone of the observatory in question*.
my $base_date;
if ($start_date_string =~ /^\s*today\s*$/i) {
$base_date = DateTime->now( time_zone => 'UTC' );
} else {
# Parse the date out of the date string with a regular expression;
# allow one-digit days and months in case they leave off the
# leading zero, but require four-digit years:
$start_date_string =~ /(\d{1,2})-(\d{1,2})-(\d{4})/;
my ($month,$day,$year) = ($1,$2,$3);
if ( (not defined $1) or (not defined $2) or (not defined $3)
or ($month < 1) or ($month > 12) or ($day < 1)
or ($day > 31) or ($year <= 0)) {
# Give them a hint if maybe they used European-style
# DD-MM-YYYY format:
my $hint = '';
if ($month > 12) {
$hint = "Maybe you listed days before months?";
}
die "Could not parse date [$start_date_string]; "
. "must be 'today' or in MM-DD-YYYY format. $hint";
}
# Start at noon, local time, on requested day:
$base_date = DateTime->new(
year => $year,
month => $month,
day => $day,
hour => '12',
time_zone => $observatory_timezone,
);
$base_date->set_time_zone('UTC');
}
# Get new objects for the start and end by cloning and then adding the
# necessary offset. The clone-> operation is necessary because
# otherwise we've just created a new reference to a single object and
# any changes would change the original, too. For specifying the
# offsets, we need to take two things into account. First, we want to
# be sure that we have a large enough window for calculating
# sunrises/sunsets that we really reach all the dates we'll need
# (i.e., we need to find the *next* sunset after our last possible
# eclipse, even if that might nominally lie outside the window the
# user has requested. Second, the way DateTime arithmetic works is
# that only integer numbers of days can be added on. To address both
# of these, we add 2 to the input values, and then truncate to just
# the integer part, which has the effect of adding between 1 and 2
# days to our actual window on each end.
my $start_date = $base_date->clone;
$start_date->subtract( days => int($days_in_past + 2) );
my $end_date = $base_date->clone;
$end_date->add( days => int($days_to_print + 2) );
# Now calculate the desired sunrises and sunsets, and save them as
# DateTime::Set objects.
# We start by creating empty sets, and a DateTime object we'll
# increment to step through our time interval:
my $sunsets = DateTime::Set->empty_set;
my $sunrises = DateTime::Set->empty_set;
my $current_date = $start_date->clone;
# Now loop over the days in the interval and calculating the sunrises
# and sunsets:
while ($current_date <= $end_date) {
# Set our "sun" object to this date:
$sun->datetime($current_date);
# Find the next sunrise and sunset times, and put them into the
# sets we are building up:
my $next_sunset = $sun->set_time(horizon => $twilight_rad);
my $next_sunrise = $sun->rise_time(horizon => $twilight_rad);
# If the Sun doesn't rise or doesn't set on that day (e.g. at polar
# latitudes) then the above might be undefined and we can't merge it
# with the rest of the set:
if (not defined $next_sunset) {
# Check to see if this is because Sun is always down:
if ($sun->el(format => 'rad') < $twilight_rad) {
# Dark so we can observe; take the meridian crossing as the
# delimeter of the night, rather than sunrise/sunset:
$sunsets = $sunsets->union( $sun->meridian_time() );
}
} else {
$sunsets = $sunsets->union( $next_sunset );
}
if (not defined $next_sunrise) {
# Check to see if this is because Sun is always down:
if ($sun->el(format => 'rad') < $twilight_rad) {
# Dark so we can observe; take the meridian crossing as the
# delimeter of the night, rather than sunrise/sunset:
$sunrises = $sunrises->union( $sun->meridian_time() );
}
} else {
$sunrises = $sunrises->union( $next_sunrise );
}
# Increment by a day and go back to the beginning of the loop.
# Actually, since the time of sunset and sunrise shift a little bit
# day to day, if we increment by a day, and if we happen to be doing
# this exactly at sunset or sunrise, we could miss an event (e.g.,
# the next sunrise could come 23h59m later and we increment by 24h).
# To be on the safe side, increment by less than 24 hours. The edge
# case near the poles is that the first/last days with sunlight can
# differ by up to an hour in day length from the previous day. If we
# end up calculating some duplicate times (which we will eventually
# if we have a long enough span) it doesn't really matter, since
# DateTime::Set recognizes duplicate values and doesn't actually add
# another one to the set.
$current_date->add( hours => 23, minutes => 0 );
}
# Print out the appropriate header for either the calendar output or
# the HTML page:
my $print_calendar;
if ($print_html == 0) {
$print_calendar = 1;
print $q->header(-type =>"text/csv",
-charset => "UTF-8",
-attachment => "transits.csv");
my $header_text = "Subject,Start Date,Start Time,End Date," .
"End Time,All Day Event,Description\r\n";
print $header_text;
} else { # HTML output
$print_calendar = 0;
if ($print_html == 1) {
# Print the HTML header, including the cookies. This output is
# where the cookies actually are returned the user's browser and set.
print $q->header(-type => "text/html",
-charset => "UTF-8",
-Cache_Control => "no-cache",
-cookie => [$latitude_cookie,
$longitude_cookie,
$utc_cookie,
$unc_cookie,
$and_cookie,
$observatory_cookie,
$timezone_cookie,
$days_cookie,
$minimum_start_elevation_cookie,
$minimum_end_elevation_cookie,
$minimum_ha_cookie,
$maximum_ha_cookie,
$baseline_hrs_cookie,
$days_in_past_cookie,
$minimum_priority_cookie,
$maximum_Vmag_cookie,
$minimum_depth_cookie,
$max_airmass_cookie,
$show_fov_cookie,
$fov_width_cookie,
$fov_height_cookie,
$fov_PA_cookie,
$twilight_cookie]
);
print $q->start_html( -title => "Upcoming transits",
);
} else {
# Print raw CSV (not for calendar):
print $q->header(-type =>"text/csv",
-charset => "UTF-8",
-attachment => "transits.csv");
}
} # End printing headers.
# Now, the main part of the code, for dealing with transits. First,
# we need to set up some variables we'll use.
# Set up a hash that contains input parameters describing the
# observatory, dates, and so on - parameters that are not
# target-specific but which govern which events are observable and/or
# desired:
my %constraints = (
days_to_print=>$days_to_print,
days_in_past=>$days_in_past,
observatory_latitude => $observatory_latitude,
observatory_longitude => $observatory_longitude,
observatory_timezone => $observatory_timezone,
observing_from_space => $observing_from_space,
minimum_start_elevation
=> $minimum_start_elevation,
minimum_end_elevation
=> $minimum_end_elevation,
sunrises => $sunrises,
sunsets => $sunsets,
telescope => $telescope,
debug => $debug,
base_date => $base_date,
twilight_rad => $twilight_rad,
sun => $sun,
moon => $moon,
baseline_hrs => $baseline_hrs,
phase => $phase,
);
# Initialize the arrays we'll use to sort the eclipse times and
# the text to print:
my @eclipse_times = ();
my @eclipse_info = ();
my @eclipse_input_data = ();
my @non_eclipse_info = ();
# Separate code fetches the target info, and then stores it in a text
# file. Here we read in the text file and parse it.
# Specify the record separator we will use to split up the line
# into fields; This record separator (a comma followed by a
# period) makes it straightforward to allow commas to be embedded
# within fields themselves. Spaces on either side of the field
# separator are not significant, i.e. they are ignored.
my $rec_separator = ',.';
# If they have entered an ephemeris for a single object, we use
# that to construct an equivalent target info line; otherwise we
# read target lines from the specified target file:
my @lines = ();
my $no_twilight = 0;
if ($sunrises->is_empty_set() and $sunsets->is_empty_set()) {
# No darkness; just leave target list empty;
# Set a flag so we know why there are no entries:
$no_twilight = 1;
} elsif ($single_object == 1) {
# They have checked the radio button that specifies manual entry
# of the ephemeris for a single object, so get the entered
# info. Put these into a hash, since that's what would be returned
# if we use a regular target file:
my %t;
$t{'RA'} = num_only($q->param("ra"));
$t{'Dec'} = num_only($q->param("dec"));
$t{'name'} = encode_entities($q->param("target"));
$t{'period'} = num_only($q->param("period"));
$t{'epoch'} = num_only($q->param("epoch"));
$t{'duration'} = num_only($q->param("duration"));
$t{'depth'} = num_only($q->param("depth"));
$t{'comments'} = "Manually-entered single object";
push @lines, \%t;
# Effectively disable the filtering on transit depth and
# priority below by re-setting the threshhold values here.
# The target doesn't have this info, so we don't want it
# to get filtered out inadvertently.
$minimum_depth = -2;
$minimum_priority = -2;
} else {
# Read the file. Could include a different path here; be sure this
# is readable by whatever process runs the CGI script.
# Try to open the file; we wrote it with UTF-8 encoding, so make
# sure we read it back the same way:
@lines = @{ Text::CSV::csv(in => $target_file,
encoding => "UTF8",
headers => "auto",
)};
}
# Initialize a few variables for keeping track of errors:
my $error_line_count = 0;
my @error_names_list = ();
# Flag to indicate whether we exit the loop after hitting a limit on
# how many events to print:
my $reached_max_eclipses = 0;
# Now, loop over the lines of the input, assuming one target per
# line. Lines read from CSV are hash references, with each hash keyed
# by the header names in the file.
# Fields assumed to be present in the CSV (and thus in the hash keys)
# are:
# name: target name
# RA: J2000 RA in h:m:s
# Dec: J2000 Dec in d:m:s
# vmag: V magnitude
# epoch: JD for central transit (i.e. zero point of the ephemeris)
# epoch_uncertainty: in days; optional, but propagated if present.
# period: Period in days
# period_uncertainty: in days; optional, but propagated if present.
# duration: Transit duration, in hours
# comments: Comments on the target; these are not used in the processing,
# but they are passed along to the output.
# depth: Depth of the transit in ppt. Not used in calculations but
# can be a filter for which events to display.
# If different field names are used in the input file, name
# reassignments can be given below (as with RA and Dec here).
# Our list of references to targets we want to search, i.e. those
# that survive any cuts on target name, magnitude, etc.,
# and have sufficient data:
my @targets_to_search = ();
TARGET_LOOP:
foreach my $target_ref (@lines) {
# We default to everything being assumed to be a periodic /
# eclipsing / transiting event for this code; non-period targets are
# an option in other code that's not used here.
$target_ref->{phot_requested} = 1;
# Likewise we assign a placeholder priority for the non-TESS
# targets:
if (not ($tess or $exowatch)) {
$target_ref->{priority} = 1;
}
# Just different names for these than are in input file:
$target_ref->{ra_string} = $target_ref->{RA};
$target_ref->{dec_string} = $target_ref->{Dec};
# For printing input info if needed:
my $cleaned_line = join(", ", map { "$target_ref->{$_}" } keys %$target_ref);
# Combine the hash of target-specific info with the
# previously-created hash of general observatory circumstances, to
# make a master hash that we will use to determine object
# observability. Note that combining two hashes in this way causes
# problems if there are duplicate keys in the two hashes (the value
# in the second key-value pair will overwrite the one in the first
# pair with the same key); watch out for this if you add fields to
# either hash.
my %target_info = (%$target_ref, %constraints);
# Now save the input line, to show the ephemeris used.
# Note: it seems a little weird here to save this input line in a
# hash with only one entry, and then push that one-element hash
# into an array. However, this array-of-hashes format ends up
# making it easier to loop over later in the HTML output template.
# (See explanation and examples for HTML::Template at