├── requirements.txt ├── README.md └── chronocalc.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | ephem 3 | timezonefinder 4 | PrettyTable 5 | arrow -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chronocalc 2 | 3 | I wrote this script after I was busy updating my article on chronolocation, which can be [found here](https://sector035.nl/articles/chronolocation-of-media). 4 | 5 | It calculates the dates and times when the sun or moon is in a given position in the sky. It can be used during chronolocation tasks, to quickly find possible dates and times within a 15 minutes accuracy. For finding a more precise moment, one should use a tool like suncalc.org to find a closer match, or use the option `--accurate`. 6 | 7 | ## Installation 8 | 9 | Install all the requirements first: 10 | 11 | `pip install -r requirements.txt` 12 | 13 | Or with the newest versions: 14 | 15 | `python3.8 -m pip install -r requirements.txt` 16 | 17 | ## Usage 18 | 19 | To determine the date and time, you need to have the following information already: 20 | - The exact latitude and longitude of the location 21 | - The year you want to calculate 22 | - Height (altitude) of the sun or moon 23 | - Direction (azimuth) of the sun or moon* 24 | 25 | The script will pull the elevation information from an online source, and after this the script will iterate through a whole year, in 15 minute increments, and returns one of the following: 26 | 27 | ## Sun 28 | 29 | Two dates on either side of the solstice, where the given altitude and azimuth matches the closest to the calculated time. If it's not possible to find a match, there might me two possibilities: 30 | - The initial measurements are incorrect and the situation can't exist 31 | - The location is extrmely close to the equator, and the sun moves too fast for the 15 minute increment 32 | 33 | If the latitude given is close to the equator, about 2 degrees or less off, you can use the `--accurate` option. This will change the calculation interval to 2 minutes, given a better chance to find a possible match. This will take longer to retrieve a result, but if there is a match, it should be shown. 34 | 35 | ## Moon 36 | 37 | To retrieve a list of dates where the moon is at a given position, use the optional argument `--moon`. 38 | 39 | It will return all dates and times the moon is in the given position, and its illumination percentage. Since the moon can be in the same position multiple times a year, all dates that are within a are within a 2 degree limit of the altitude and azimuth are returned. 40 | 41 | If there are no matches with the given parameter, it will return a message saying so. Change some variables, or check your findings, and run the script again. The `--accurate` argument has no use here. 42 | 43 | ### Examples 44 | 45 | To find the dates where the sun is in a given position: 46 | 47 | `python chronocalc.py -y 2020 -lat 50.9423 -lon 6.9579 -alt 32 -az 200` 48 | 49 | Result: 50 | 51 | ``` 52 | +---------------------------+----------+---------+ 53 | | Date and time | Altitude | Azimuth | 54 | +---------------------------+----------+---------+ 55 | | 2020-03-08T14:00:00+01:00 | 32.08 | 202.87 | 56 | | 2020-10-05T14:30:00+02:00 | 32.09 | 200.58 | 57 | +---------------------------+----------+---------+ 58 | ``` 59 | 60 | Find all the dates where the moon is at a given position in the sky: 61 | 62 | `python chronocalc.py -y 2018 -lat 50.9423 -lon 6.9579 -alt 30 -az 200 --moon` 63 | 64 | Result: 65 | 66 | ``` 67 | +---------------------------+----------+---------+--------------+ 68 | | Date and time | Altitude | Azimuth | Illumination | 69 | +---------------------------+----------+---------+--------------+ 70 | | 2018-01-08T08:15:00+01:00 | 31.28 | 218.99 | 56.6% | 71 | | 2018-01-22T19:15:00+01:00 | 28.95 | 221.78 | 28.0% | 72 | | 2018-02-05T07:00:00+01:00 | 28.28 | 219.48 | 73.7% | 73 | | 2018-03-04T04:45:00+01:00 | 30.69 | 219.79 | 94.1% | 74 | | 2018-03-18T15:45:00+01:00 | 31.54 | 219.73 | 1.4% | 75 | | 2018-04-14T14:45:00+02:00 | 28.95 | 220.78 | 2.9% | 76 | | 2018-04-28T02:15:00+02:00 | 30.16 | 219.45 | 95.3% | 77 | | 2018-05-12T13:15:00+02:00 | 31.91 | 220.0 | 11.5% | 78 | | 2018-05-25T00:15:00+02:00 | 31.48 | 220.87 | 78.6% | 79 | | 2018-06-08T11:15:00+02:00 | 28.91 | 221.16 | 34.2% | 80 | | 2018-06-21T23:00:00+02:00 | 28.62 | 219.23 | 65.3% | 81 | | 2018-07-18T21:00:00+02:00 | 29.86 | 220.99 | 39.3% | 82 | | 2018-08-02T07:45:00+02:00 | 30.6 | 219.83 | 75.1% | 83 | | 2018-08-29T05:45:00+02:00 | 29.26 | 219.47 | 93.1% | 84 | | 2018-09-11T17:30:00+02:00 | 29.16 | 221.32 | 5.1% | 85 | | 2018-09-26T04:30:00+02:00 | 31.86 | 221.75 | 98.8% | 86 | | 2018-10-23T02:15:00+02:00 | 31.01 | 218.37 | 96.8% | 87 | | 2018-11-05T13:00:00+01:00 | 28.59 | 221.58 | 5.8% | 88 | | 2018-11-18T23:15:00+01:00 | 28.16 | 219.37 | 79.9% | 89 | | 2018-12-02T10:45:00+01:00 | 31.51 | 219.53 | 24.7% | 90 | | 2018-12-16T21:45:00+01:00 | 30.99 | 218.8 | 63.4% | 91 | +---------------------------+----------+---------+--------------+ 92 | ``` -------------------------------------------------------------------------------- /chronocalc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import ephem 5 | from ephem import degree 6 | import json 7 | from prettytable import PrettyTable 8 | import requests 9 | import sys 10 | 11 | from dateutil import rrule, tz 12 | from datetime import datetime, timedelta 13 | import arrow 14 | 15 | from timezonefinder import TimezoneFinder 16 | 17 | def get_timezone(lon, lat): 18 | tf = TimezoneFinder() 19 | local_timezone = tf.timezone_at(lng=float(lon), lat=float(lat)) 20 | return(local_timezone) 21 | 22 | def get_elevation(lon, lat): 23 | constructurl = 'https://api.open-elevation.com/api/v1/lookup?locations='+str(lat)+','+str(lon) 24 | response = requests.get(url=constructurl) 25 | elevation = response.json() 26 | return(elevation['results'][0]['elevation']) 27 | 28 | def get_moon_position(year, lon, lat, elevation, searchalt, searchaz): 29 | location = ephem.Observer() 30 | location.lon = lon 31 | location.lat = lat 32 | location.elevation = elevation 33 | pos = ephem.Moon() 34 | 35 | utc_timezone = tz.gettz('UTC') 36 | local_timezone = tz.gettz(get_timezone(lon, lat)) 37 | 38 | startmoment = datetime(year, 1, 1, 0, 00, 00) 39 | endmoment = datetime(year, 12, 31 ,23, 59, 00) 40 | 41 | tab = PrettyTable() 42 | tab.field_names = ['Date and time', 'Altitude', 'Azimuth', 'Illumination'] 43 | tab.align['Altitude'] = 'l' 44 | tab.align['Azimuth'] = 'l' 45 | 46 | empty_table = True 47 | 48 | for timewindow in rrule.rrule(rrule.MINUTELY, interval=15, dtstart=startmoment, until=endmoment): 49 | location.date = timewindow 50 | pos.compute(location) 51 | if abs(pos.alt /degree - searchalt) < 2 and abs(pos.az /degree - searchaz) < 2: 52 | tab.add_row([arrow.get(timewindow).to('UTC').to(local_timezone), round(pos.alt/degree,2), round(pos.az/degree,2), str(round(pos.moon_phase*100,1))+'%']) 53 | empty_table = False 54 | 55 | if empty_table: 56 | print('No possible solution found. Please check the given parameters.') 57 | else: 58 | print('\nOn the following dates and times the moon is closest to the given parameters:\n') 59 | print(tab) 60 | #print(pos.moon_phase) 61 | 62 | def calculate_sun(results, lon, lat, elevation, searchalt, searchaz, calc_start, calc_end, accuracy): 63 | location = ephem.Observer() 64 | location.lon = lon 65 | location.lat = lat 66 | location.elevation = elevation 67 | pos = ephem.Sun() 68 | 69 | utc_timezone = tz.gettz('UTC') 70 | local_timezone = tz.gettz(get_timezone(lon, lat)) 71 | 72 | for timewindow in rrule.rrule(rrule.MINUTELY, interval=accuracy, dtstart=calc_start, until=calc_end): 73 | location.date = timewindow 74 | pos.compute(location) 75 | if abs(pos.alt/degree - searchalt) < 3 and abs(pos.az/degree - searchaz) < 3: 76 | closeby = (abs(pos.alt/degree - searchalt) * abs(pos.az/degree - searchaz)) 77 | results.append((round(pos.alt/degree,2), round(pos.az/degree,2), timewindow, closeby)) 78 | 79 | results.sort(key=lambda results:results[3]) 80 | return results 81 | 82 | def get_sun_position(year, lon, lat, elevation, searchalt, searchaz, accuracy): 83 | utc_timezone = tz.gettz('UTC') 84 | local_timezone = tz.gettz(get_timezone(lon, lat)) 85 | 86 | date_solstice = ephem.next_solstice(datetime(year, 1, 1, 0, 00, 00)) 87 | date_solstice = date_solstice.datetime() 88 | date_solstice = date_solstice.replace(minute=0, second=0, microsecond=0) 89 | 90 | date_start = datetime(year, 1, 1, 0, 00, 00) 91 | date_end = datetime(year, 12, 31, 23, 59, 00) 92 | 93 | results1 = [] 94 | calculate_sun(results1, lon, lat, elevation, searchalt, searchaz, date_start, date_solstice, accuracy) 95 | 96 | results2 = [] 97 | calculate_sun(results2, lon, lat, elevation, searchalt, searchaz, date_solstice, date_end, accuracy) 98 | 99 | if len(results1) < 2: 100 | sys.exit('No possible solution found. Please check the given parameters.') 101 | # Define a nice table 102 | tab = PrettyTable() 103 | tab.field_names = ['Date and time', 'Altitude', 'Azimuth'] 104 | tab.add_rows( 105 | [ 106 | [arrow.get(results1[0][2]).to('UTC').to(local_timezone), results1[0][0], results1[0][1]], 107 | [arrow.get(results2[0][2]).to('UTC').to(local_timezone), results2[0][0], results2[0][1]], 108 | ] 109 | ) 110 | print('\nOn the following dates and times the sun is closest to the given parameters:\n') 111 | print(tab) 112 | 113 | def main(): 114 | parser = argparse.ArgumentParser(description='ChronoCalc - Determine dates & times of sun or moon positions within a year, given the location.') 115 | needed = parser.add_argument_group('Required arguments') 116 | needed.add_argument('-y', type=int, dest='year', help='The year that you want to calculate (ie: 2017)') 117 | needed.add_argument('-lat', type=str, dest='lat', help='Latitude of the observers location in decimals (ie: 7.2332)') 118 | needed.add_argument('-lon', type=str, dest='lon', help='Longitude of the observers location in decimals (ie: 49.8731)') 119 | needed.add_argument('-alt', type=float, dest='alt', help='Altitude from the observers perspective in degrees (ie: 25.8)') 120 | needed.add_argument('-az', type=float, dest='az', help='Azimuth from the observers perspective indegrees (ie: 220.5)') 121 | parser.add_argument('--moon', dest='moon', action='store_true', default=False, help='Run the script to for the moon, instead of the sun.') 122 | parser.add_argument('--accurate', dest='accuracy', action='store_const', const=2, default=15, help='Run the script in high accuracy mode (2 min interval). Useful when nothing is found, for instance when the latitude is near the equator, around -2 to 2 degrees.') 123 | args = parser.parse_args() 124 | 125 | elevation = get_elevation(args.lon, args.lat) 126 | 127 | if args.moon: 128 | get_moon_position(args.year, args.lon, args.lat, elevation, args.alt, args.az) 129 | else: 130 | get_sun_position(args.year, args.lon, args.lat, elevation, args.alt, args.az, args.accuracy) 131 | 132 | if __name__=="__main__": 133 | main() --------------------------------------------------------------------------------