Emphasized ISOT standard for passing times to WWT

This commit is contained in:
O . O 2019-09-14 14:58:38 -04:00
Родитель 63f46a0133
Коммит 6f3267edf2
6 изменённых файлов: 52 добавлений и 87 удалений

Просмотреть файл

@ -3,6 +3,7 @@ from traitlets import HasTraits, observe, validate, TraitError
from astropy import units as u
from astropy.time import Time
from astropy.coordinates import SkyCoord
from datetime import datetime
# We import the trait classes from .traits since we do various customizations
from .traits import Color, Bool, Float, Unicode, AstropyQuantity
@ -15,6 +16,7 @@ from .instruments import Instruments
import json
import os
import pytz
import shutil
import tempfile
@ -220,14 +222,18 @@ class BaseWWTWidget(HasTraits):
astropy :class:`astropy.time.Time` object. If not specified, this
uses the current time
"""
# Ensure the object received is a datetime or Time; convert it to UTC
if dt is None:
dt = Time.now()
if isinstance(dt, Time):
dt = dt.datetime
self._send_msg(event='set_datetime',
year=dt.year, month=dt.month, day=dt.day,
hour=dt.hour, minute=dt.minute, second=dt.second,
millisecond=int(dt.microsecond / 1000.))
utc_dt = datetime.utcnow()
elif isinstance(dt, datetime):
utc_dt = dt.astimezone(timezone=pytz.UTC)
elif isinstance(dt, Time):
utc_dt = dt.to_datetime(timezone=pytz.UTC)
else:
raise ValueError('Time must be a datetime or astropy.Time object')
iso_t = Time(utc_dt, format='datetime').isot
self._send_msg(event='set_datetime', isot=iso_t)
def center_on_coordinates(self, coord, fov=60 * u.deg, instant=True):
"""

Просмотреть файл

@ -4,6 +4,7 @@ import tempfile
from os import path
import shutil
import pytz
import re
if sys.version_info[0] == 2: # noqa
@ -21,7 +22,6 @@ from matplotlib.colors import Colormap
from astropy import units as u
from astropy.table import Column
from astropy.time import Time, TimeDelta
from datetime import datetime
from traitlets import HasTraits, validate, observe
@ -57,7 +57,7 @@ VALID_MARKER_SCALES = ['screen', 'world']
VALID_STRETCHES = ['linear', 'log', 'power', 'sqrt', 'histeq']
# Create regex test to validate ISOT strings in time series tables
# Save string types for validating ISOT strings in time series tables
if sys.version_info[0] == 2:
STR_TYPE = basestring
NP_STR_TYPE = np.string_
@ -65,17 +65,11 @@ else:
STR_TYPE = str
NP_STR_TYPE = np.unicode_
VALID_ISOT_FORMAT = (r'^(-?(?:[1-9][0-9]*)?[0-9]{4})-(1[0-2]|0[1-9])-'
+ r'(3[01]|0[1-9]|[12][0-9])T'
+ r'(2[0-3]|[01][0-9]):([0-5][0-9]):'
+ r'([0-5][0-9])'
+ r'(\.[0-9]+)?(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$')
iso_test = re.compile(VALID_ISOT_FORMAT)
# The following are columns that we add dynamically and internally, so we need
# to make sure they have unique names that won't clash with existing columns
SIZE_COLUMN_NAME = str(uuid.uuid4())
CMAP_COLUMN_NAME = str(uuid.uuid4())
TIME_COLUMN_NAME = str(uuid.uuid4())
def guess_lon_lat_columns(colnames):
@ -344,7 +338,7 @@ class TableLayer(HasTraits):
cmap_vmax = Float(None, help='The maximum level of the colormap. Found '
'automagically once cmap_att is set (`float`)',
allow_none=True).tag(wwt=None)
cmap = Any(cm.magma, help='The Matplotlib colormap '
cmap = Any(cm.viridis, help='The Matplotlib colormap '
'(:class:`matplotlib.colors.ListedColormap`)').tag(wwt=None)
# Visual attributes
@ -372,9 +366,11 @@ class TableLayer(HasTraits):
time_att = Unicode(help='The column to use for time (`str`)').tag(wwt=None)
time_series = Bool(False, help='Whether the layer contains time series '
'elements (`bool`)').tag(wwt='timeSeries')
decay = AstropyQuantity(16 * u.day, help='How long a time series point '
'takes to fade away after appearing (0 if never) '
'(:class:`~astropy.units.Quantity`)').tag(wwt='decay')
time_decay = AstropyQuantity(16 * u.day, help='How long a time series '
'point takes to fade away after appearing (0 '
'if never) '
'(:class:`~astropy.units.Quantity`)'
).tag(wwt='decay')
# TODO: support:
# xAxisColumn
@ -393,17 +389,6 @@ class TableLayer(HasTraits):
raise ValueError('frame should be one of {0}'.format('/'.join(sorted(str(x) for x in VALID_FRAMES))))
self.frame = frame.capitalize()
# ISSUE: For some reason, WWT seems to prefer the that time column
# be 0th-10th/12th column in the table.
### (varies with placement of latitude and longitude columns,
### which also need to be 0th-12th)
# So the current proposition is to limit table's max number of columns
### (the max will change depending on how time_att is implemented)
max_cols = 13
if len(self.table.colnames) > max_cols:
raise ValueError('Table must have fewer than {} '.format(max_cols)
+ 'columns to ensure predictable behavior')
self.parent = parent
self.id = str(uuid.uuid4())
@ -428,7 +413,7 @@ class TableLayer(HasTraits):
self._on_trait_change({'name': 'cmap_att', 'new': self.cmap_att})
self._on_trait_change({'name': 'time_att', 'new': self.time_att})
self._on_trait_change({'name': 'time_series', 'new': self.time_series})
self._on_trait_change({'name': 'decay', 'new': self.decay})
self._on_trait_change({'name': 'time_decay', 'new': self.time_decay})
self.observe(self._on_trait_change, type='change')
@ -509,27 +494,26 @@ class TableLayer(HasTraits):
if (isinstance(col, STR_TYPE)
or np.issubdtype(col.dtype, NP_STR_TYPE)):
col_list = col.tolist()
is_iso = all(iso_test.match(t) for t in col_list)
if is_iso:
try:
Time(col, format='isot')
return proposal['value']
else:
except ValueError:
raise ValueError('String times must conform to the ISOT'
'standard (YYYY-MM-DD`T`HH:MM:SS:MS)')
elif isinstance(col, (datetime, Time)):
elif (all(isinstance(t, datetime) for t in col)
or all(isinstance(t, Time) for t in col)):
return proposal['value']
else:
raise ValueError('A time column must have string, '
raise ValueError('A time column must only have string, '
'datetime.datetime, or astropy Time values')
@validate('decay')
@validate('time_decay')
def _check_decay(self, proposal):
if proposal['value'].unit.physical_type == 'time':
return proposal['value']
else:
raise ValueError('decay should be in units of time')
raise ValueError('time_decay should be in units of time')
@observe('alt_att')
def _on_alt_att_change(self, *value):
@ -716,58 +700,35 @@ class TableLayer(HasTraits):
@observe('time_att')
def _on_time_att_change(self, *value):
# Convert time column to UTC so WWT displays points at expected times
### ISSUE: Currently not robust to changes in DST.
# Needs GMT instead of UTC?
# The conversion only seems needed in Qt, so check the widget version
if type(self.parent).__name__.find('Qt') < 0:
self.parent._send_msg(event='table_layer_set', id=self.id,
setting='startDateColumn',
value=self.time_att)
return
if len(self.time_att) == 0 or self.time_series == False:
self.parent._send_msg(event='table_layer_set', id=self.id,
setting='startDateColumn', value=-1)
return
# Convert time column to UTC so WWT displays points at expected times
col = self.table[self.time_att]
if isinstance(col, datetime):
wwt_times = Column([t.isoformat() for t in col])
wwt_times = Column([t.astimezone(timezone=pytz.UTC).isoformat()
for t in col])
elif isinstance(col, Time):
wwt_times = Column([t.isot for t in col])
wwt_times = Column([t.to_datetime(timezone=pytz.UTC).isot
for t in col])
else:
col_list = col.tolist()
wwt_times = Column(col_list)
# Find the difference between user's curent local time and UTC
# (rounding should also be robust for UTC+X:15, +X:30 time zones)
now = datetime.now()
utc_now = datetime.utcnow()
now_diff = (utc_now - now).total_seconds() / 3600
offset = (np.round(now_diff * 4) / 4) * u.hour
# Add time offset to column so WWT will read values as UTC
wwt_times = (Time(wwt_times) + TimeDelta(offset)).isot
# Update the table passed to WWT with the new, modified time column
### ISSUE: better to create a new column (like cmap_att/size_att)
### or replace the entries in the original time_att column?
### given the column limit discussed earlier, creating new columns
### further limits the number of columns a user can provide
self.table[self.time_att] = wwt_times
#self.table[TIME_COLUMN_NAME] = wwt_times
wwt_times = Time(wwt_times).isot
self.table[TIME_COLUMN_NAME] = wwt_times
self.parent._send_msg(event='table_layer_update', id=self.id,
table=self._table_b64)
self.parent._send_msg(event='table_layer_set', id=self.id,
setting='startDateColumn',
value=self.time_att)#,
#value=TIME_COLUMN_NAME)
value=TIME_COLUMN_NAME)
@property
def _table_b64(self):
@ -840,7 +801,7 @@ class TableLayer(HasTraits):
value = VALID_ALT_UNITS[self._check_xyz_unit({'value': value})]
elif isinstance(value, u.Quantity):
value = value.value
elif changed['name'] == 'decay':
elif changed['name'] == 'time_decay':
value = value.to(u.day).value
self.parent._send_msg(event='table_layer_set',
id=self.id,

Просмотреть файл

@ -81,7 +81,8 @@
" np.where(jan['longitude'] < 165)[0])\n",
"lats = np.intersect1d(np.where(jan['latitude'] > 15)[0],\n",
" np.where(jan['latitude'] < 45)[0])\n",
"japan = jan[np.intersect1d(lons, lats)]"
"japan = jan[np.intersect1d(lons, lats)]\n",
"japan.remove_column('place') # causes parsing issues"
]
},
{
@ -93,8 +94,7 @@
"# whip up some test tables\n",
"j2 = japan.copy(); j3 = japan.copy()\n",
"j2['time'] = Time(japan['time']).datetime # datetime objs\n",
"j3['time'] = [t.isoformat() for t in j2['time']] # iso strings\n",
"j3 = j3[j3.colnames[:10]].copy() # limiting to 10 cols as per new TableLayer edits"
"j3['time'] = [t.isoformat() for t in j2['time']] # iso strings"
]
},
{
@ -145,8 +145,8 @@
"# add the earthquake layer\n",
"jp_lay = wwt.layers.add_table_layer(table=j3, frame='Earth', lon_att='longitude',\n",
" lat_att='latitude', size_scale=50, color='#ce1141',\n",
" time_series=True, time_att='time',\n",
" far_side_visible=True, decay=3 * u.second)"
" far_side_visible=True, time_series=True,\n",
" time_att='time', time_decay=3*u.second)"
]
},
{
@ -183,7 +183,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.3"
"version": "3.6.3"
}
},
"nbformat": 4,

Просмотреть файл

@ -21,7 +21,6 @@ wwt.foreground_opacity = 0
j2 = japan.copy(); j3 = japan.copy()
j2['time'] = Time(japan['time']).datetime # datetime objs
j3['time'] = [t.isoformat() for t in j2['time']] # iso strings
j3 = j3[j3.colnames[:10]].copy()
# change to solar system mode
wwt.set_view('solar system')
@ -31,10 +30,10 @@ wwt.center_on_coordinates(SkyCoord(0,0,unit=u.deg), fov=.0001 * u.deg)
wwt.solar_system.track_object('Earth')
# add the earthquake layer
jp_lay = wwt.layers.add_table_layer(table=j3, frame='Earth', lon_att='longitude', lat_att='latitude', size_scale=50, color='#ce1141', time_series=True, time_att='time', far_side_visible=True, decay=3*u.day)
jp_lay = wwt.layers.add_table_layer(table=j3, frame='Earth', lon_att='longitude', lat_att='latitude', size_scale=50, color='#ce1141', far_side_visible=True, time_series=True, time_att='time', time_decay=3*u.day)
# rewind time to when the earthquakes took place
wwt.set_current_time(start - TimeDelta(1 * u.hr))
wwt.set_current_time(start)# - TimeDelta(1 * u.hr))
# ideally, the points would begin to pop up now

Просмотреть файл

@ -21,6 +21,7 @@ for i, val in enumerate(jan['time']):
lons = np.intersect1d(np.where(jan['longitude'] > 135)[0], np.where(jan['longitude'] < 165)[0])
lats = np.intersect1d(np.where(jan['latitude'] > 15)[0], np.where(jan['latitude'] < 45)[0])
japan = jan[np.intersect1d(lons, lats)]
japan.remove_column('place') # causes parsing issues
'''
# translate table to b64 for use in JS when working directly with the SDK

Просмотреть файл

@ -166,12 +166,10 @@ console.log(JSON.stringify(msg))
break;
case 'set_datetime':
// for whatever reason, Date's month argument begins from 0 (i.e. Feb. is 1)
// so we make sure to subtract 1 from msg['month']
var date = new Date(msg['year'], msg['month'] - 1, msg['day'],
msg['hour'], msg['minute'], msg['second'],
msg['millisecond']);
var date = new Date(msg['isot']);
console.log(date);
stc = wwtlib.SpaceTimeController;
stc.set_timeRate(1);
stc.set_now(date);