-
Notifications
You must be signed in to change notification settings - Fork 5
/
infoDialog.py
331 lines (297 loc) · 14.4 KB
/
infoDialog.py
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
"""
/***************************************************************************
* *
* 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
"""
import os
from datetime import datetime, timedelta
from dateutil.tz import tzlocal
from dateutil.relativedelta import relativedelta
from zoneinfo import ZoneInfo, available_timezones
from skyfield.api import load, load_file, wgs84
from skyfield import almanac
from timezonefinder import TimezoneFinder
from qgis.PyQt.QtGui import QIcon, QFont, QColor
from qgis.PyQt.QtWidgets import QDockWidget, QApplication
from qgis.PyQt.QtCore import pyqtSlot, Qt, QTime, QDate
from qgis.PyQt.uic import loadUiType
from qgis.core import Qgis, QgsPointXY, QgsLineString, QgsMultiPolygon, QgsPolygon, QgsGeometry, QgsCoordinateReferenceSystem, QgsCoordinateTransform, QgsProject
from qgis.gui import QgsRubberBand
from .captureCoordinate import CaptureCoordinate
from .utils import epsg4326, settings
from .wintz import win_tz_map
from .dms import parseDMSString
# import traceback
FORM_CLASS, _ = loadUiType(os.path.join(
os.path.dirname(__file__), 'ui/info.ui'))
class SolarInfoDialog(QDockWidget, FORM_CLASS):
def __init__(self, iface, parent):
super(SolarInfoDialog, self).__init__(parent)
self.setupUi(self)
self.iface = iface
self.canvas = iface.mapCanvas()
self.savedMapTool = None
self.cur_location = None
self.tzf = TimezoneFinder()
# Set up a polygon rubber band
self.rubber = QgsRubberBand(self.canvas)
self.rubber.setColor(QColor(255, 70, 0, 200))
self.rubber.setWidth(3)
self.rubber.setBrushStyle(Qt.NoBrush)
# Set up a connection with the coordinate capture tool
self.captureCoordinate = CaptureCoordinate(self.canvas)
self.captureCoordinate.capturePoint.connect(self.capturedPoint)
self.captureCoordinate.captureStopped.connect(self.stopCapture)
self.coordCaptureButton.clicked.connect(self.startCapture)
self.dateEdit.setCalendarPopup(True)
self.coordCaptureButton.setIcon(QIcon(os.path.dirname(__file__) + "/icons/coordCapture.svg"))
self.currentDateTimeButton.setIcon(QIcon(os.path.dirname(__file__) + "/icons/CurrentTime.png"))
self.coordLineEdit.returnPressed.connect(self.coordCommitButton)
self.dt_utc = datetime.now(ZoneInfo("UTC"))
self.initialTimeZone()
def showEvent(self, e):
self.dt_utc = datetime.now(ZoneInfo("UTC"))
self.updateGuiDateTime()
def closeEvent(self, e):
if self.savedMapTool:
self.canvas.setMapTool(self.savedMapTool)
self.savedMapTool = None
self.rubber.reset()
QDockWidget.closeEvent(self, e)
def initialTimeZone(self):
"""
This is to get a standardized UTC time and time zone from the system time
that conforms to the known timezones used by zoneinfo. At bare minimum the
system time zone offset is used. Timezones are a pain.
"""
tz = tzlocal()
dt = datetime.now(tz)
try:
name = dt.tzinfo.zone
except Exception:
name = dt.tzname()
if name in win_tz_map:
name = win_tz_map[name]
if name not in available_timezones():
offset = int(dt.tzinfo.utcoffset(dt).total_seconds()/3600.0)
name = 'Etc/GMT{:+d}'.format(-offset)
self.cur_tzname = name
self.tz = ZoneInfo(name)
def getLocalDateTime(self):
dt = self.dt_utc.astimezone(self.tz)
return(dt)
def updateGuiDateTime(self):
self.timezoneEdit.setText(self.cur_tzname)
dt = self.dt_utc.astimezone(self.tz)
offset = dt.strftime("%z")
self.tzOffsetEdit.setText(offset)
self.dateEdit.blockSignals(True)
self.timeEdit.blockSignals(True)
if self.useUtcCheckBox.isChecked():
self.dateEdit.setDate(QDate(self.dt_utc.year, self.dt_utc.month, self.dt_utc.day))
self.timeEdit.setTime(QTime(self.dt_utc.hour, self.dt_utc.minute, self.dt_utc.second))
else:
self.dateEdit.setDate(QDate(dt.year, dt.month, dt.day))
self.timeEdit.setTime(QTime(dt.hour, dt.minute, dt.second))
self.dateEdit.blockSignals(False)
self.timeEdit.blockSignals(False)
def updateSunInfo(self):
self.updateGuiDateTime()
self.clearInfo()
try:
if self.cur_location:
# Set coordinate
coord = '{:.8f}, {:.8f}'.format(self.cur_location.y(), self.cur_location.x())
self.coordLineEdit.setText(coord)
loc = wgs84.latlon(self.cur_location.y(), self.cur_location.x())
ts = settings.timescale()
dt = self.getLocalDateTime()
year = dt.year
cur_time = ts.from_datetime(dt)
# Load ephemeris
eph = settings.ephem()
earth = eph['earth']
sun = eph['sun']
moon = eph['moon']
# Get sun azimuth and altitude
observer = earth + loc
astrometric = observer.at(cur_time).observe(sun)
alt, az, d = astrometric.apparent().altaz()
self.sunAzimuthLabel.setText('{:.6f}'.format(az.degrees))
self.sunElevationLabel.setText('{:.6f}'.format(alt.degrees))
# Get moon azimuth and altitude
astrometric = observer.at(cur_time).observe(moon)
alt, az, d = astrometric.apparent().altaz()
self.moonAzimuthLabel.setText('{:.6f}'.format(az.degrees))
self.moonElevationLabel.setText('{:.6f}'.format(alt.degrees))
# Get solar noon
midnight = dt.replace(hour=0, minute=0, second=0, microsecond=0)
next_midnight = midnight + timedelta(days=1)
t0 = ts.from_datetime(midnight) # Starting time to search for events
t1 = ts.from_datetime(next_midnight) # Ending time to search for events
f = almanac.meridian_transits(eph, sun, loc)
times, events = almanac.find_discrete(t0, t1, f)
if times:
# Select transits instead of antitransits.
times = times[events == 1]
t = times[0]
self.noonLabel.setText(self.formatDateTime(t))
# Find the twlight hours
f = almanac.dark_twilight_day(eph, loc)
times, events = almanac.find_discrete(t0, t1, f)
previous_e = f(t0)
has_start = False
has_end = False
for t, e in zip(times, events):
if previous_e < e:
if e == 4: # Day starts
day_start = t
has_start = True
self.sunriseLabel.setText(self.formatDateTime(t))
elif e == 3: # Dawn
self.dawnLabel.setText(self.formatDateTime(t))
else:
if e == 3: # Civil twilight starts
day_end = t
has_end = True
self.civilTwilightLabel.setText(self.formatDateTime(t))
self.sunsetLabel.setText(self.formatDateTime(t))
elif e == 2: # Nautical twilight starts
self.nauticalTwilightLabel.setText(self.formatDateTime(t))
elif e == 1: # Astronomical twilight starts
self.astronomicalTwilightLabel.setText(self.formatDateTime(t))
elif e == 0: # Night starts
self.nightLabel.setText(self.formatDateTime(t))
previous_e = e
if has_start and has_end:
diff = relativedelta(day_end.utc_datetime(), day_start.utc_datetime())
if diff.days == 1:
str = '24 hours'
else:
str = '{}h {}m {}s'.format(diff.hours, diff.minutes, diff.seconds)
self.daylightDurationLabel.setText(str)
else:
f = almanac.sunrise_sunset(eph, loc)
str = 'Polar day' if f(t0) else 'Polar night'
self.sunriseLabel.setText(str)
self.sunsetLabel.setText(str)
# Calculate the phase of the moon
t = ts.from_datetime(self.dt_utc)
phase = almanac.moon_phase(eph, t)
self.moonPhaseLabel.setText('{:.1f} degrees'.format(phase.degrees))
# Calculate the seasons
t0 = ts.utc(year, 1, 1)
t1 = ts.utc(year, 12, 31)
t, y = almanac.find_discrete(t0, t1, almanac.seasons(eph))
if self.cur_location.y() >= 0:
self.vernalEquinoxLabel.setText(self.formatDateTime(t[0]))
self.summerSolsticeLabel.setText(self.formatDateTime(t[1]))
self.autumnalEquinoxLabel.setText(self.formatDateTime(t[2]))
self.winterSolsticeLabel.setText(self.formatDateTime(t[3]))
else:
self.vernalEquinoxLabel.setText(self.formatDateTime(t[2]))
self.summerSolsticeLabel.setText(self.formatDateTime(t[3]))
self.autumnalEquinoxLabel.setText(self.formatDateTime(t[0]))
self.winterSolsticeLabel.setText(self.formatDateTime(t[1]))
except Exception:
self.iface.messageBar().pushMessage("", "The ephemeris file does not cover the selected date range. Go to Settings and download and select an ephemeris file that contains your date range.", level=Qgis.Critical, duration=6)
def clearInfo(self):
self.sunAzimuthLabel.setText('')
self.sunElevationLabel.setText('')
self.dawnLabel.setText('')
self.sunriseLabel.setText('')
self.sunsetLabel.setText('')
self.civilTwilightLabel.setText('')
self.nauticalTwilightLabel.setText('')
self.astronomicalTwilightLabel.setText('')
self.nightLabel.setText('')
self.daylightDurationLabel.setText('')
self.noonLabel.setText('')
self.moonAzimuthLabel.setText('')
self.moonElevationLabel.setText('')
self.vernalEquinoxLabel.setText('')
self.summerSolsticeLabel.setText('')
self.autumnalEquinoxLabel.setText('')
self.winterSolsticeLabel.setText('')
self.moonPhaseLabel.setText('')
def formatDateTime(self, dt):
if self.useUtcCheckBox.isChecked():
return(dt.utc_iso())
else:
# return python utc datetime, round to nearest second, covert to local time zone
tz_dt = (dt.utc_datetime() + timedelta(milliseconds=500)).astimezone(self.tz)
fmt = '%Y-%m-%d %H:%M:%S'
return( tz_dt.strftime(fmt) )
def on_currentDateTimeButton_pressed(self):
self.dt_utc = datetime.now(ZoneInfo("UTC"))
self.updateSunInfo()
def startCapture(self):
# print('startCapture')
if self.coordCaptureButton.isChecked():
self.savedMapTool = self.canvas.mapTool()
self.canvas.setMapTool(self.captureCoordinate)
else:
if self.savedMapTool:
self.canvas.setMapTool(self.savedMapTool)
self.savedMapTool = None
@pyqtSlot(QgsPointXY)
def capturedPoint(self, pt):
# print('capturedPoint')
lon = pt.x()
lat = pt.y()
if lat > 90 or lat < -90 or lon > 180 or lon < -180:
self.cur_location = None
self.coordLineEdit.setText('')
self.updateSunInfo()
return
if self.isVisible() and self.coordCaptureButton.isChecked():
self.cur_location = pt
self.cur_tzname = self.tzf.timezone_at(lng=lon, lat=lat)
self.tz = ZoneInfo(self.cur_tzname)
self.updateSunInfo()
else:
self.cur_location = None
@pyqtSlot()
def stopCapture(self):
# print('stopCapture')
self.coordCaptureButton.setChecked(False)
self.rubber.reset()
def on_useUtcCheckBox_stateChanged(self, state):
self.updateSunInfo()
def on_dateEdit_dateChanged(self, date):
if self.useUtcCheckBox.isChecked():
self.dt_utc = self.dt_utc.replace(year=date.year(), month=date.month(), day=date.day())
else:
dt = self.getLocalDateTime()
dt = dt.replace(year=date.year(), month=date.month(), day=date.day())
self.dt_utc = dt.astimezone(ZoneInfo("UTC"))
self.updateSunInfo()
def on_timeEdit_timeChanged(self, time):
if self.useUtcCheckBox.isChecked():
self.dt_utc = self.dt_utc.replace(hour=time.hour(), minute=time.minute(), second=time.second())
else:
dt = self.getLocalDateTime()
dt = dt.replace(hour=time.hour(), minute=time.minute(), second=time.second())
self.dt_utc = dt.astimezone(ZoneInfo("UTC"))
self.updateSunInfo()
def coordCommitButton(self):
# print('coordCommitButton Pressed')
try:
coord = self.coordLineEdit.text().strip()
if not coord:
self.clearInfo()
return
(lat, lon) = parseDMSString(coord)
self.cur_location = QgsPointXY(lon, lat)
self.cur_tzname = self.tzf.timezone_at(lng=lon, lat=lat)
self.tz = ZoneInfo(self.cur_tzname)
self.updateSunInfo()
except Exception:
self.clearInfo()
self.iface.messageBar().pushMessage("", "Invalid 'latitude, longitude'", level=Qgis.Warning, duration=2)
return