Merge pull request #215 from jsub1/interactive-figure
Interactive Figure Proof of Concept
This commit is contained in:
Коммит
fa54ee6815
|
@ -53,7 +53,7 @@ be generated in the center of your view::
|
|||
:align: center
|
||||
|
||||
Once an annotation is no longer needed, it can be
|
||||
removed via its :meth:`pywwt.Circle.remove` method. The main
|
||||
removed via its :meth:`~pywwt.Annotation.remove` method. The main
|
||||
WorldWide Telescope object (``wwt`` in this case) also has a dedicated method
|
||||
for erasing every existing annotation from view called
|
||||
:meth:`~pywwt.BaseWWTWidget.clear_annotations`.
|
||||
|
|
|
@ -48,6 +48,7 @@ class Annotation(HasTraits):
|
|||
super(Annotation, self).__init__(**kwargs)
|
||||
else:
|
||||
raise KeyError('a key doesn\'t match any annotation trait name')
|
||||
self.parent._annotation_set.add(self)
|
||||
|
||||
def _on_trait_change(self, changed):
|
||||
# This method gets called anytime a trait gets changed. Since this class
|
||||
|
@ -62,6 +63,26 @@ class Annotation(HasTraits):
|
|||
setting=wwt_name,
|
||||
value=changed['new'])
|
||||
|
||||
def _serialize_state(self):
|
||||
state = {'shape': self.shape,
|
||||
'id': self.id,
|
||||
'settings': {}}
|
||||
for trait in self.traits().values():
|
||||
wwt_name = trait.metadata.get('wwt')
|
||||
if wwt_name:
|
||||
trait_val = trait.get(self)
|
||||
if isinstance(trait_val, u.Quantity):
|
||||
trait_val = trait_val.value
|
||||
state['settings'][wwt_name] = trait_val
|
||||
return state
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Removes the specified annotation from the current view.
|
||||
"""
|
||||
self.parent._send_msg(event='remove_annotation', id=self.id)
|
||||
self.parent._annotation_set.discard(self)
|
||||
|
||||
|
||||
class Circle(Annotation):
|
||||
"""
|
||||
|
@ -80,13 +101,20 @@ class Circle(Annotation):
|
|||
'(`str` or `tuple`)').tag(wwt='fillColor')
|
||||
line_color = Color('white', help='Assigns line color for the circle '
|
||||
'(`str` or `tuple`)').tag(wwt='lineColor')
|
||||
line_width = AstropyQuantity(1*u.pixel,
|
||||
line_width = AstropyQuantity(1 * u.pixel,
|
||||
help='Assigns line width in pixels '
|
||||
'(:class:`~astropy.units.Quantity`)').tag(wwt='lineWidth')
|
||||
radius = AstropyQuantity(80*u.pixel,
|
||||
radius = AstropyQuantity(80 * u.pixel,
|
||||
help='Sets the radius for the circle '
|
||||
'(:class:`~astropy.units.Quantity`)').tag(wwt='radius')
|
||||
|
||||
def __init__(self, parent=None, center=None, **kwargs):
|
||||
super(Circle, self).__init__(parent, **kwargs)
|
||||
if center:
|
||||
self.set_center(center)
|
||||
else:
|
||||
self._center = parent.get_center().icrs
|
||||
|
||||
@validate('line_width')
|
||||
def _validate_linewidth(self, proposal):
|
||||
if proposal['value'].unit.is_equivalent(u.pixel):
|
||||
|
@ -116,12 +144,7 @@ class Circle(Annotation):
|
|||
self.parent._send_msg(event='circle_set_center', id=self.id,
|
||||
ra=coord_icrs.ra.degree,
|
||||
dec=coord_icrs.dec.degree)
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Removes the specified annotation from the current view.
|
||||
"""
|
||||
self.parent._send_msg(event='remove_annotation', id=self.id)
|
||||
self._center = coord_icrs
|
||||
|
||||
def _on_trait_change(self, changed):
|
||||
if changed['name'] == 'radius':
|
||||
|
@ -136,6 +159,13 @@ class Circle(Annotation):
|
|||
|
||||
super(Circle, self)._on_trait_change(changed)
|
||||
|
||||
def _serialize_state(self):
|
||||
state = super(Circle, self)._serialize_state()
|
||||
state['settings']['skyRelative'] = self.radius.unit.is_equivalent(u.degree)
|
||||
state['center'] = {'ra': self._center.ra.deg,
|
||||
'dec': self._center.dec.deg}
|
||||
return state
|
||||
|
||||
|
||||
class Polygon(Annotation):
|
||||
"""
|
||||
|
@ -154,10 +184,14 @@ class Polygon(Annotation):
|
|||
'(`str` or `tuple`)').tag(wwt='fillColor')
|
||||
line_color = Color('white', help='Assigns line color for the polygon '
|
||||
'(`str` or `tuple`)').tag(wwt='lineColor')
|
||||
line_width = AstropyQuantity(1*u.pixel,
|
||||
line_width = AstropyQuantity(1 * u.pixel,
|
||||
help='Assigns line width in pixels '
|
||||
'(:class:`~astropy.units.Quantity`)').tag(wwt='lineWidth')
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
self._points = []
|
||||
super(Polygon, self).__init__(parent, **kwargs)
|
||||
|
||||
@validate('line_width')
|
||||
def _validate_linewidth(self, proposal):
|
||||
if proposal['value'].unit.is_equivalent(u.pixel):
|
||||
|
@ -178,21 +212,17 @@ class Polygon(Annotation):
|
|||
The coordinates of the desired point(s).
|
||||
"""
|
||||
coord_icrs = coord.icrs
|
||||
if coord_icrs.isscalar: # if coord only has one point
|
||||
if coord_icrs.isscalar: # if coord only has one point
|
||||
self.parent._send_msg(event='polygon_add_point', id=self.id,
|
||||
ra=coord_icrs.ra.degree,
|
||||
dec=coord_icrs.dec.degree)
|
||||
self._points.append(coord_icrs)
|
||||
else:
|
||||
for point in coord_icrs:
|
||||
self.parent._send_msg(event='polygon_add_point', id=self.id,
|
||||
ra=point.ra.degree,
|
||||
dec=point.dec.degree)
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Removes the specified annotation from the current view.
|
||||
"""
|
||||
self.parent._send_msg(event='remove_annotation', id=self.id)
|
||||
self._points.append(point)
|
||||
|
||||
def _on_trait_change(self, changed):
|
||||
if isinstance(changed['new'], u.Quantity):
|
||||
|
@ -200,6 +230,14 @@ class Polygon(Annotation):
|
|||
|
||||
super(Polygon, self)._on_trait_change(changed)
|
||||
|
||||
def _serialize_state(self):
|
||||
state = super(Polygon, self)._serialize_state()
|
||||
state['points'] = []
|
||||
for point in self._points:
|
||||
state['points'].append({'ra': point.ra.degree,
|
||||
'dec': point.dec.degree})
|
||||
return state
|
||||
|
||||
|
||||
class Line(Annotation):
|
||||
"""
|
||||
|
@ -214,10 +252,14 @@ class Line(Annotation):
|
|||
color = ColorWithOpacity('white',
|
||||
help='Assigns color for the line '
|
||||
'(`str` or `tuple`)').tag(wwt='lineColor')
|
||||
width = AstropyQuantity(1*u.pixel,
|
||||
width = AstropyQuantity(1 * u.pixel,
|
||||
help='Assigns width for the line in pixels '
|
||||
'(:class:`~astropy.units.Quantity`)').tag(wwt='lineWidth')
|
||||
|
||||
def __init__(self, parent=None, **kwargs):
|
||||
self._points = []
|
||||
super(Line, self).__init__(parent, **kwargs)
|
||||
|
||||
@validate('width')
|
||||
def _validate_width(self, proposal):
|
||||
if proposal['value'].unit.is_equivalent(u.pixel):
|
||||
|
@ -235,21 +277,17 @@ class Line(Annotation):
|
|||
The coordinates of the desired point(s).
|
||||
"""
|
||||
coord_icrs = coord.icrs
|
||||
if coord_icrs.isscalar: # if coord only has one point
|
||||
if coord_icrs.isscalar: # if coord only has one point
|
||||
self.parent._send_msg(event='line_add_point', id=self.id,
|
||||
ra=coord_icrs.ra.degree,
|
||||
dec=coord_icrs.dec.degree)
|
||||
self._points.append(coord_icrs)
|
||||
else:
|
||||
for point in coord_icrs:
|
||||
self.parent._send_msg(event='line_add_point', id=self.id,
|
||||
ra=point.ra.degree,
|
||||
dec=point.dec.degree)
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Removes the specified annotation from the current view.
|
||||
"""
|
||||
self.parent._send_msg(event='remove_annotation', id=self.id)
|
||||
self._points.append(point)
|
||||
|
||||
def _on_trait_change(self, changed):
|
||||
if isinstance(changed['new'], u.Quantity):
|
||||
|
@ -257,12 +295,21 @@ class Line(Annotation):
|
|||
|
||||
super(Line, self)._on_trait_change(changed)
|
||||
|
||||
def _serialize_state(self):
|
||||
state = super(Line, self)._serialize_state()
|
||||
state['points'] = []
|
||||
for point in self._points:
|
||||
state['points'].append({'ra': point.ra.degree,
|
||||
'dec': point.dec.degree})
|
||||
return state
|
||||
|
||||
|
||||
class FieldOfView():
|
||||
"""
|
||||
A collection of polygon annotations. Takes the name of a pre-loaded
|
||||
telescope and displays its field of view.
|
||||
"""
|
||||
|
||||
# a more efficient method than CircleCollection of changing trait values?
|
||||
|
||||
def __init__(self, parent, telescope, center, rot, **kwargs):
|
||||
|
@ -323,7 +370,7 @@ class FieldOfView():
|
|||
if abs(dec) > 90:
|
||||
if dec < -90:
|
||||
decs[i] = -90. - (dec + 90.)
|
||||
else: # dec > 90
|
||||
else: # dec > 90
|
||||
decs[i] = 90. - (dec - 90.)
|
||||
ras[i] += 180.
|
||||
|
||||
|
@ -362,7 +409,7 @@ class CircleCollection():
|
|||
self.points = points
|
||||
else:
|
||||
raise IndexError('For performance reasons, only 10,000 '
|
||||
'annotations can be added at once for the time being.')
|
||||
'annotations can be added at once for the time being.')
|
||||
self.parent = parent
|
||||
self.collection = []
|
||||
self._gen_circles(self.points, **kwargs)
|
||||
|
@ -384,8 +431,7 @@ class CircleCollection():
|
|||
|
||||
def _gen_circles(self, points, **kwargs):
|
||||
for elem in points:
|
||||
circle = Circle(self.parent, **kwargs)
|
||||
circle.set_center(elem)
|
||||
circle = Circle(self.parent, elem, **kwargs)
|
||||
self.collection.append(circle)
|
||||
|
||||
def add_points(self, points, **kwargs):
|
||||
|
|
115
pywwt/core.py
115
pywwt/core.py
|
@ -13,6 +13,11 @@ from .solar_system import SolarSystem
|
|||
from .layers import LayerManager
|
||||
from .instruments import Instruments
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
# The WWT web control API is described here:
|
||||
# https://worldwidetelescope.gitbook.io/html5-control-reference/
|
||||
|
||||
|
@ -44,7 +49,9 @@ class BaseWWTWidget(HasTraits):
|
|||
self._instruments = Instruments()
|
||||
self.current_mode = 'sky'
|
||||
self._paused = False
|
||||
self._last_sent_view_mode = 'sky'
|
||||
self.layers = LayerManager(parent=self)
|
||||
self._annotation_set = set()
|
||||
|
||||
# NOTE: we deliberately don't force _on_trait_change to be called here
|
||||
# for the WWT settings, as the default values are hard-coded in wwt.html
|
||||
|
@ -140,6 +147,7 @@ class BaseWWTWidget(HasTraits):
|
|||
"""
|
||||
Clears all annotations from the current view.
|
||||
"""
|
||||
self._annotation_set.clear()
|
||||
return self._send_msg(event='clear_annotations')
|
||||
|
||||
def get_center(self):
|
||||
|
@ -320,6 +328,7 @@ class BaseWWTWidget(HasTraits):
|
|||
elif mode == 'mars':
|
||||
mode = 'Visible Imagery'
|
||||
self._send_msg(event='set_viewer_mode', mode=mode)
|
||||
self._last_sent_view_mode = mode
|
||||
if mode == 'sky' or mode == 'panorama':
|
||||
self.current_mode = mode
|
||||
else:
|
||||
|
@ -327,6 +336,7 @@ class BaseWWTWidget(HasTraits):
|
|||
elif mode in VIEW_MODES_3D:
|
||||
self._send_msg(event='set_viewer_mode', mode=solar_system_mode)
|
||||
self.current_mode = mode
|
||||
self._last_sent_view_mode = solar_system_mode
|
||||
else:
|
||||
raise ValueError('mode should be one of {0}'.format('/'.join(VIEW_MODES_2D + VIEW_MODES_3D)))
|
||||
|
||||
|
@ -444,9 +454,7 @@ class BaseWWTWidget(HasTraits):
|
|||
attributes to be set upon shape initialization.
|
||||
"""
|
||||
# TODO: could buffer JS call here
|
||||
circle = Circle(parent=self, **kwargs)
|
||||
if center:
|
||||
circle.set_center(center)
|
||||
circle = Circle(parent=self, center=center, **kwargs)
|
||||
return circle
|
||||
|
||||
def add_polygon(self, points=None, **kwargs):
|
||||
|
@ -554,3 +562,104 @@ class BaseWWTWidget(HasTraits):
|
|||
for trait_name, trait in self.traits().items():
|
||||
if trait.metadata.get('wwt_reset'):
|
||||
setattr(self, trait_name, trait.default_value)
|
||||
|
||||
def save_as_html_bundle(self, dest, title=None, max_width=None, max_height=None):
|
||||
"""
|
||||
Save the current view as a web page with supporting files.
|
||||
|
||||
This feature is currently under development, so not all
|
||||
settings/features that can be set in pyWWT will be saved
|
||||
|
||||
Parameters
|
||||
----------
|
||||
dest : `str`
|
||||
The path to output the bundle to. The path must represent a
|
||||
directory (which will be created if it does not exist) or a zip file.
|
||||
title : `str`, optional
|
||||
The desired title for the HTML page. If blank, a generic title will be used.
|
||||
max_width : `int`, optional
|
||||
The maximum width of the WWT viewport on the exported HTML page in pixels.
|
||||
If left blank, the WWT viewport will fill the enitre width of the browser.
|
||||
max_height : `int`, optional
|
||||
The maximum height of the WWT viewport on the exported HTML page in pixels.
|
||||
If left blank, the WWT viewport will fill the enitre height of the browser.
|
||||
"""
|
||||
dest_root, dest_extension = os.path.splitext(dest)
|
||||
if (dest_extension and dest_extension != ".zip"):
|
||||
raise ValueError("'dest' must be either a directory or a .zip file")
|
||||
|
||||
is_compressed = dest_extension == '.zip'
|
||||
if is_compressed:
|
||||
figure_dir = tempfile.mkdtemp()
|
||||
else:
|
||||
if not os.path.exists(dest):
|
||||
os.makedirs(os.path.abspath(dest))
|
||||
figure_dir = dest
|
||||
|
||||
nbexten_dir = os.path.join(os.path.dirname(__file__), 'nbextension', 'static')
|
||||
fig_src_dir = os.path.join(nbexten_dir, 'interactive_figure')
|
||||
shutil.copy(os.path.join(fig_src_dir, "index.html"), figure_dir)
|
||||
|
||||
script_dir = os.path.join(figure_dir, 'scripts')
|
||||
if not os.path.exists(script_dir):
|
||||
os.mkdir(script_dir)
|
||||
shutil.copy(os.path.join(nbexten_dir, 'wwt_json_api.js'), script_dir)
|
||||
shutil.copy(os.path.join(fig_src_dir, "interactive_figure.js"), script_dir)
|
||||
|
||||
self._serialize_to_json(os.path.join(figure_dir,'wwt_figure.json'), title, max_width, max_height)
|
||||
|
||||
if len(self.layers) > 0:
|
||||
data_dir = os.path.join(figure_dir,'data')
|
||||
if not os.path.exists(data_dir):
|
||||
os.mkdir(data_dir)
|
||||
self._save_added_data(data_dir)
|
||||
|
||||
if is_compressed:
|
||||
zip_parent_dir = os.path.abspath(os.path.dirname(dest_root))
|
||||
if not os.path.exists(zip_parent_dir):
|
||||
os.makedirs(zip_parent_dir)
|
||||
shutil.make_archive(dest_root, 'zip', root_dir=figure_dir)
|
||||
|
||||
def _serialize_state(self, title, max_width, max_height):
|
||||
state = dict()
|
||||
state['html_settings'] = {'title': title,
|
||||
'max_width': max_width,
|
||||
'max_height': max_height}
|
||||
|
||||
state['wwt_settings'] = {}
|
||||
for trait in self.traits().values():
|
||||
wwt_name = trait.metadata.get('wwt')
|
||||
if wwt_name:
|
||||
trait_val = trait.get(self)
|
||||
if isinstance(trait_val, u.Quantity):
|
||||
trait_val = trait_val.value
|
||||
state['wwt_settings'][wwt_name] = trait_val
|
||||
|
||||
center = self.get_center()
|
||||
fov = self.get_fov()
|
||||
state['view_settings'] = {'mode': self._last_sent_view_mode,
|
||||
'ra': center.icrs.ra.deg,
|
||||
'dec': center.icrs.dec.deg,
|
||||
'fov': fov.to_value(u.deg)}
|
||||
|
||||
state['foreground_settings'] = {'foreground': self.foreground,
|
||||
'background': self.background,
|
||||
'foreground_alpha': self.foreground_opacity * 100}
|
||||
|
||||
state['layers'] = self.layers._serialize_state()
|
||||
|
||||
if self.current_mode in VIEW_MODES_3D:
|
||||
self.solar_system._add_settings_to_serialization(state)
|
||||
|
||||
state['annotations'] = []
|
||||
for annot in self._annotation_set:
|
||||
state['annotations'].append(annot._serialize_state())
|
||||
return state
|
||||
|
||||
def _serialize_to_json(self, file, title, max_width, max_height):
|
||||
state = self._serialize_state(title, max_width, max_height)
|
||||
with open(file,'w') as file_obj:
|
||||
json.dump(state,file_obj)
|
||||
|
||||
def _save_added_data(self, dir):
|
||||
self.layers._save_all_data_for_serialization(dir)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import sys
|
||||
import uuid
|
||||
import tempfile
|
||||
from os import path
|
||||
import shutil
|
||||
|
||||
import re
|
||||
|
||||
|
@ -217,6 +219,17 @@ class LayerManager(object):
|
|||
|
||||
__repr__ = __str__
|
||||
|
||||
def _serialize_state(self):
|
||||
layer_states = []
|
||||
for layer in self._layers:
|
||||
layer_states.append(layer._serialize_state())
|
||||
|
||||
return layer_states
|
||||
|
||||
def _save_all_data_for_serialization (self, dir):
|
||||
for layer in self._layers:
|
||||
layer._save_data_for_serialization(dir)
|
||||
|
||||
|
||||
class TableLayer(HasTraits):
|
||||
"""
|
||||
|
@ -412,7 +425,7 @@ class TableLayer(HasTraits):
|
|||
|
||||
# Update the size column in the table
|
||||
|
||||
if len(self.size_att) == 0 or self.size_vmin is None or self.size_vmax is None:
|
||||
if self._uniform_size():
|
||||
self.parent._send_msg(event='table_layer_set', id=self.id,
|
||||
setting='sizeColumn', value=-1)
|
||||
return
|
||||
|
@ -460,7 +473,7 @@ class TableLayer(HasTraits):
|
|||
|
||||
# Update the cmap column in the table
|
||||
|
||||
if len(self.cmap_att) == 0 or self.cmap_vmin is None or self.cmap_vmax is None:
|
||||
if self._uniform_color():
|
||||
|
||||
self.parent._send_msg(event='table_layer_set', id=self.id,
|
||||
setting='colorMapColumn', value=-1)
|
||||
|
@ -499,6 +512,12 @@ class TableLayer(HasTraits):
|
|||
|
||||
return b64encode(csv.encode('ascii', errors='replace')).decode('ascii')
|
||||
|
||||
def _uniform_color(self):
|
||||
return not self.cmap_att or self.cmap_vmin is None or self.cmap_vmax is None
|
||||
|
||||
def _uniform_size(self):
|
||||
return not self.size_att or self.size_vmin is None or self.size_vmax is None
|
||||
|
||||
def _initialize_layer(self):
|
||||
self.parent._send_msg(event='table_layer_create',
|
||||
id=self.id, table=self._table_b64, frame=self.frame)
|
||||
|
@ -555,6 +574,43 @@ class TableLayer(HasTraits):
|
|||
setting=wwt_name,
|
||||
value=value)
|
||||
|
||||
def _serialize_state(self):
|
||||
state = {'id': self.id,
|
||||
'layer_type': 'table',
|
||||
'frame': self.frame,
|
||||
'settings': {}}
|
||||
|
||||
for trait in self.traits().values():
|
||||
wwt_name = trait.metadata.get('wwt')
|
||||
if wwt_name:
|
||||
value = trait.get(self)
|
||||
if wwt_name == 'raUnits' and value is not None:
|
||||
value = VALID_LON_UNITS[value]
|
||||
elif wwt_name == 'altUnit' and value is not None:
|
||||
value = VALID_ALT_UNITS[value]
|
||||
state['settings'][wwt_name] = value
|
||||
|
||||
if self._uniform_color():
|
||||
state['settings']['_colorMap'] = 0
|
||||
state['settings']['colorMapColumn'] = -1
|
||||
else:
|
||||
state['settings']['_colorMap'] = 3
|
||||
state['settings']['colorMapColumn'] = CMAP_COLUMN_NAME
|
||||
|
||||
if self._uniform_size():
|
||||
state['settings']['sizeColumn'] = -1
|
||||
else:
|
||||
state['settings']['sizeColumn'] = SIZE_COLUMN_NAME
|
||||
state['settings']['pointScaleType'] = 0
|
||||
|
||||
return state
|
||||
|
||||
def _save_data_for_serialization(self, dir):
|
||||
file_path = path.join(dir,"{0}.csv".format(self.id))
|
||||
table_str = csv_table_win_newline(self.table)
|
||||
with open(file_path, 'wb') as file: # binary mode to preserve windows line endings
|
||||
file.write(table_str.encode('ascii', errors='replace'))
|
||||
|
||||
def __str__(self):
|
||||
return 'TableLayer with {0} markers'.format(len(self.table))
|
||||
|
||||
|
@ -570,7 +626,7 @@ class ImageLayer(HasTraits):
|
|||
vmin = Float(None, allow_none=True)
|
||||
vmax = Float(None, allow_none=True)
|
||||
stretch = Unicode('linear')
|
||||
opacity = Float(1, help='The opacity of the markers').tag(wwt='opacity')
|
||||
opacity = Float(1, help='The opacity of the image').tag(wwt='opacity')
|
||||
|
||||
def __init__(self, parent=None, image=None, **kwargs):
|
||||
|
||||
|
@ -663,6 +719,29 @@ class ImageLayer(HasTraits):
|
|||
setting=wwt_name,
|
||||
value=changed['new'])
|
||||
|
||||
def _serialize_state(self):
|
||||
state = {'id': self.id,
|
||||
'layer_type': 'image',
|
||||
'settings': {}
|
||||
}
|
||||
|
||||
#A bit overkill for just the opacity, but more future-proof in case we add more wwt traits
|
||||
for trait in self.traits().values():
|
||||
wwt_name = trait.metadata.get('wwt')
|
||||
if wwt_name:
|
||||
state['settings'][wwt_name] = trait.get(self)
|
||||
|
||||
if self.vmin is not None and self.vmax is not None:
|
||||
state['stretch_info'] = {'vmin': self.vmin,
|
||||
'vmax': self.vmax,
|
||||
'stretch':VALID_STRETCHES.index(self.stretch)}
|
||||
|
||||
return state
|
||||
|
||||
def _save_data_for_serialization(self, dir):
|
||||
file_path = path.join(dir,"{0}.fits".format(self.id))
|
||||
shutil.copyfile(self._sanitized_image,file_path)
|
||||
|
||||
def __str__(self):
|
||||
return 'ImageLayer'
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html >
|
||||
<html style="height: 100%; padding: 0px; margin: 0px; background-color: #000000;">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8" http-equiv="X-UA-Compatible" content="chrome=1, IE=edge"/>
|
||||
|
||||
<title></title>
|
||||
<script src="https://WorldWideTelescope.github.io/pywwt/wwtsdk.js"></script>
|
||||
<script src="https://code.jquery.com/jquery-1.8.3.min.js"></script>
|
||||
<script src="scripts/wwt_json_api.js"></script>
|
||||
<script src="scripts/interactive_figure.js"></script>
|
||||
|
||||
<!-- The following is to avoid scrollbars -->
|
||||
<style type="text/css">
|
||||
body {overflow:hidden;}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(setHtmlSettings);
|
||||
$(window).resize(setHtmlSettings);
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body onload="initialize()" style="margin: 0; padding: 0">
|
||||
<div id="wwt-canvas" style="width: 100; height: 100; border-style: none; border-width: 0px;"></div>
|
||||
<div id="wwt-error-text" style="color: silver;"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,287 @@
|
|||
var wwtInitialState;
|
||||
|
||||
var wwt;
|
||||
|
||||
function initialize() {
|
||||
// The true enables WebGL
|
||||
wwt = wwtlib.WWTControl.initControlParam("wwt-canvas", true);
|
||||
wwt.add_ready(loadWwtFigure);
|
||||
wwt.add_ready(keyScroll);
|
||||
}
|
||||
|
||||
$.getJSON('wwt_figure.json')
|
||||
.done(function (data) { wwtInitialState = data; })
|
||||
.error(function (err) {
|
||||
wwtInitialState = null;
|
||||
handleConfigLoadError();
|
||||
});
|
||||
|
||||
function keyScroll() {
|
||||
|
||||
i = 0;
|
||||
var canvas = document.body.getElementsByTagName("canvas")[0];
|
||||
|
||||
function newEvent(action, attributes, deprecated) {
|
||||
if (!deprecated)
|
||||
var event = new CustomEvent(action);
|
||||
else {
|
||||
var event = document.createEvent("CustomEvent");
|
||||
event.initEvent(action, false, false);
|
||||
}
|
||||
if (attributes)
|
||||
for (var attr in attributes)
|
||||
event[attr] = attributes[attr];
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
||||
var wheelUp = newEvent("wwt-zoom", { deltaY: 53, delta: 53 }, true);
|
||||
var wheelDn = newEvent("wwt-zoom", { deltaY: -53, delta: -53 }, true);
|
||||
|
||||
var mouseLf = newEvent("wwt-move", { movementX: 53, movementY: 0 }, true);
|
||||
var mouseUp = newEvent("wwt-move", { movementX: 0, movementY: 53 }, true);
|
||||
var mouseRg = newEvent("wwt-move", { movementX: -53, movementY: 0 }, true);
|
||||
var mouseDn = newEvent("wwt-move", { movementX: 0, movementY: -53 }, true);
|
||||
|
||||
zoomCodes = {
|
||||
"KeyZ": wheelUp, "KeyX": wheelDn,
|
||||
90: wheelUp, 88: wheelDn
|
||||
};
|
||||
moveCodes = {
|
||||
"KeyJ": mouseLf, "KeyI": mouseUp,
|
||||
"KeyL": mouseRg, "KeyK": mouseDn,
|
||||
74: mouseLf, 73: mouseUp, 76: mouseRg, 75: mouseDn
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", function (event) {
|
||||
if (zoomCodes.hasOwnProperty(event.code) ||
|
||||
zoomCodes.hasOwnProperty(event.keyCode)) {
|
||||
var action = zoomCodes.hasOwnProperty(event.code) ?
|
||||
zoomCodes[event.code] : zoomCodes[event.keyCode];
|
||||
if (event.shiftKey) { action.shiftKey = 1; }
|
||||
else { action.shiftKey = 0; }
|
||||
canvas.dispatchEvent(action);
|
||||
}
|
||||
|
||||
if (moveCodes.hasOwnProperty(event.code) ||
|
||||
moveCodes.hasOwnProperty(event.keyCode)) {
|
||||
var action = moveCodes.hasOwnProperty(event.code) ?
|
||||
moveCodes[event.code] : moveCodes[event.keyCode];
|
||||
if (event.shiftKey) { action.shiftKey = 1; }
|
||||
else { action.shiftKey = 0; }
|
||||
if (event.altKey) { action.altKey = 1; }
|
||||
else { action.altKey = 0; }
|
||||
canvas.dispatchEvent(action);
|
||||
}
|
||||
});
|
||||
|
||||
canvas.addEventListener("wwt-move", (function (proceed) {
|
||||
return function (event) {
|
||||
if (!proceed) { return false; }
|
||||
proceed = false;
|
||||
|
||||
if (event.shiftKey) { delay = 500; }
|
||||
else { delay = 100; }
|
||||
setTimeout(function () { proceed = true }, delay);
|
||||
if (event.altKey)
|
||||
wwtlib.WWTControl.singleton._tilt(event.movementX, event.movementY);
|
||||
else
|
||||
wwtlib.WWTControl.singleton.move(event.movementX, event.movementY);
|
||||
}
|
||||
})(true));
|
||||
|
||||
canvas.addEventListener("wwt-zoom", (function (proceed) {
|
||||
return function (event) {
|
||||
if (!proceed) { return false; }
|
||||
proceed = false;
|
||||
|
||||
if (event.shiftKey) { delay = 500; } // milliseconds
|
||||
else { delay = 100; }
|
||||
setTimeout(function () { proceed = true }, delay);
|
||||
|
||||
if (event.deltaY < 0) { wwtlib.WWTControl.singleton.zoom(1.43); }
|
||||
else { wwtlib.WWTControl.singleton.zoom(0.7); }
|
||||
}
|
||||
})(true));
|
||||
}
|
||||
|
||||
function loadWwtFigure() {
|
||||
if (wwtInitialState === undefined) { //JSON config file has not loaded yet, try again in 50 ms
|
||||
setTimeout(loadWwtFigure, 50);
|
||||
return;
|
||||
}
|
||||
else if (wwtInitialState === null) { //There was an error loading the config
|
||||
return;
|
||||
}
|
||||
//TODO allow loading more collections
|
||||
wwt.loadImageCollection('https://WorldWideTelescope.github.io/pywwt/surveys.xml')
|
||||
|
||||
var viewSettings = wwtInitialState['view_settings'];
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'set_viewer_mode',
|
||||
mode: viewSettings['mode']
|
||||
});
|
||||
|
||||
if (viewSettings['mode'] == 'sky') {
|
||||
var foregroundState = wwtInitialState['foreground_settings'];
|
||||
wwt.setForegroundImageByName(foregroundState['foreground']);
|
||||
wwt.setBackgroundImageByName(foregroundState['background']);
|
||||
wwt.setForegroundOpacity(foregroundState['foreground_alpha']);
|
||||
}
|
||||
|
||||
var miscSettings = wwtInitialState['wwt_settings'];
|
||||
for (name in miscSettings) {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'setting_set',
|
||||
setting: name,
|
||||
value: miscSettings[name]
|
||||
});
|
||||
}
|
||||
|
||||
wwtInitialState['layers'].forEach(function (layerInfo) {
|
||||
if (layerInfo['layer_type'] == 'image') {
|
||||
loadImageLayer(layerInfo);
|
||||
}
|
||||
else if (layerInfo['layer_type'] == 'table') {
|
||||
loadTableLayer(layerInfo);
|
||||
}
|
||||
});
|
||||
|
||||
wwtInitialState['annotations'].forEach(loadAnnotation);
|
||||
|
||||
if (!viewSettings['tracked_object_id']) { //Not tracking or trivially track sun (id=0)
|
||||
wwt.gotoRaDecZoom(viewSettings['ra'], viewSettings['dec'], viewSettings['fov'], true);
|
||||
}
|
||||
else {
|
||||
var targetCamera = wwtlib.CameraParameters.create(0, 0, viewSettings['fov'] * 6, 0, 0, wwtlib.WWTControl.singleton.renderContext.viewCamera.opacity); //Multiply fov by 6 to get zoom factor
|
||||
targetCamera.target = viewSettings['tracked_object_id'];
|
||||
targetCamera.set_RA(viewSettings['ra'] / 15.); //convert from degrees to hrs
|
||||
targetCamera.set_dec(viewSettings['dec']);
|
||||
wwtlib.WWTControl.singleton.gotoTarget3(targetCamera, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function loadImageLayer(layerInfo) {
|
||||
var id = layerInfo['id'];
|
||||
var url = 'data/' + id + '.fits';
|
||||
var onFitsLoad = function (layer) {
|
||||
var stertchInfo = layerInfo['stretch_info'];
|
||||
wwtLayer.setImageScale(stertchInfo['stretch'],
|
||||
stertchInfo['vmin'],
|
||||
stertchInfo['vmax']);
|
||||
layer.getFitsImage().transparentBlack = false;
|
||||
var settings = layerInfo['settings'];
|
||||
for (name in settings) {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'image_layer_set',
|
||||
setting: name,
|
||||
value: settings[name],
|
||||
id: id
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var wwtLayer = wwt.loadFitsLayer(url, '', false, onFitsLoad);
|
||||
wwt.layers[id] = wwtLayer;
|
||||
}
|
||||
|
||||
function loadTableLayer(layerInfo) {
|
||||
var id = layerInfo['id'];
|
||||
var url = 'data/' + id + '.csv';
|
||||
var onCsvLoad = function (data) {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'table_layer_create',
|
||||
frame: layerInfo['frame'],
|
||||
id: id,
|
||||
table: btoa(data)
|
||||
});
|
||||
var settings = layerInfo['settings'];
|
||||
for (name in settings) {
|
||||
if (settings[name] !== null) {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'table_layer_set',
|
||||
setting: name,
|
||||
value: settings[name],
|
||||
id: id
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$.ajax(url, datatype = "text")
|
||||
.fail(function () {
|
||||
$("#wwt-error-text").append("<p>Unable to load data for layer with ID: " + id + "</p>"); //TODO replace with something nicer
|
||||
})
|
||||
.done(onCsvLoad);
|
||||
}
|
||||
|
||||
function loadAnnotation(annotation) {
|
||||
var shape = annotation['shape'];
|
||||
var id = annotation['id'];
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'annotation_create',
|
||||
shape: shape,
|
||||
id: id
|
||||
});
|
||||
|
||||
if (shape == "circle") {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'circle_set_center',
|
||||
id: id,
|
||||
ra: annotation['center']['ra'],
|
||||
dec: annotation['center']['dec']
|
||||
});
|
||||
}
|
||||
else {
|
||||
annotation['points'].forEach(function (point) {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: shape + '_add_point',
|
||||
id: id,
|
||||
ra: point['ra'],
|
||||
dec: point['dec']
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
var settings = annotation['settings'];
|
||||
for (name in settings) {
|
||||
wwt_apply_json_message(wwt, {
|
||||
event: 'annotation_set',
|
||||
id: id,
|
||||
setting: name,
|
||||
value: settings[name]
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
function setHtmlSettings() {
|
||||
if (wwtInitialState === undefined) { //JSON config file has not loaded yet, try again in 50 ms
|
||||
setTimeout(setHtmlSettings, 50);
|
||||
return;
|
||||
}
|
||||
else if (wwtInitialState === null) {
|
||||
return;
|
||||
}
|
||||
var figHtmlSettings = wwtInitialState['html_settings'];
|
||||
|
||||
var title = figHtmlSettings['title'] ? figHtmlSettings['title'] : "WWT Interactive Figure";
|
||||
$(document).attr("title", title);
|
||||
|
||||
var settingsHeight = figHtmlSettings['max_height'];
|
||||
var settingsWidth = figHtmlSettings['max_width'];
|
||||
var htmlHeight = $("html").height();
|
||||
var htmlWidth = $("html").width();
|
||||
var newHeight = settingsHeight ? Math.min(settingsHeight, htmlHeight) : htmlHeight;
|
||||
var newWidth = settingsWidth ? Math.min(settingsWidth, htmlWidth) : htmlWidth;
|
||||
$("#wwt-canvas").css("height", newHeight + "px");
|
||||
$("#wwt-canvas").css("width", newWidth + "px");
|
||||
}
|
||||
|
||||
function handleConfigLoadError() {
|
||||
//TODO replace with something a bit nicer before releasing this feature
|
||||
$("#wwt-canvas").hide;
|
||||
$("#wwt-error-text").append("<p>Unable to load configuration file wwt_figure.json</p>");
|
||||
}
|
|
@ -16,6 +16,7 @@ class SolarSystem(HasTraits):
|
|||
super(SolarSystem, self).__init__()
|
||||
self.base_widget = base_wwt_widget
|
||||
self.observe(self._on_trait_change, type='change')
|
||||
self._tracked_obj_id = 0 #Default to tracking sun
|
||||
|
||||
def _on_trait_change(self, changed):
|
||||
# This method gets called anytime a trait gets changed. Since this class
|
||||
|
@ -85,5 +86,18 @@ class SolarSystem(HasTraits):
|
|||
|
||||
if obj in mappings:
|
||||
self.base_widget._send_msg(event='track_object', code=mappings[obj])
|
||||
self._tracked_obj_id = mappings[obj]
|
||||
else:
|
||||
raise ValueError('the given object cannot be tracked')
|
||||
|
||||
def _add_settings_to_serialization(self, wwt_state):
|
||||
|
||||
for trait in self.traits().values():
|
||||
wwt_name = trait.metadata.get('wwt')
|
||||
if wwt_name:
|
||||
trait_val = trait.get(self)
|
||||
if isinstance(trait_val, u.Quantity):
|
||||
trait_val = trait_val.value
|
||||
wwt_state['wwt_settings'][wwt_name] = trait_val
|
||||
|
||||
wwt_state['view_settings']['tracked_object_id'] = self._tracked_obj_id
|
||||
|
|
|
@ -0,0 +1,393 @@
|
|||
import pytest
|
||||
|
||||
from ..core import BaseWWTWidget
|
||||
from ..layers import SIZE_COLUMN_NAME, CMAP_COLUMN_NAME
|
||||
|
||||
import numpy as np
|
||||
|
||||
from astropy.wcs import WCS
|
||||
from astropy.table import Table
|
||||
from astropy import units as u
|
||||
from astropy.coordinates import SkyCoord
|
||||
|
||||
DEGREES_TO_HOURS = 1. / 15.
|
||||
STARDARD_WWT_SETTINGS = ['actualPlanetScale', 'showAltAzGrid', 'showConstellationBoundries', 'constellationBoundryColor',
|
||||
'constellationFigureColor', 'showConstellationFigures', 'showConstellationSelection',
|
||||
'constellationSelectionColor', 'showCrosshairs', 'crosshairsColor', 'showEcliptic',
|
||||
'showEclipticGrid', 'showGalacticGrid', 'galacticMode', 'showGrid', 'localHorizonMode',
|
||||
'locationAltitude', 'locationLat', 'locationLng']
|
||||
|
||||
|
||||
# Mock class so that we test serialization without instantiating an actual widget
|
||||
class MockWWTWidget(BaseWWTWidget):
|
||||
def quick_serialize(self):
|
||||
return self._serialize_state(None, None, None)
|
||||
|
||||
def _get_view_data(self, field):
|
||||
mock_vals = {'ra': 5. * DEGREES_TO_HOURS, 'dec': 10., 'fov': 15.}
|
||||
return mock_vals[field]
|
||||
|
||||
def _serve_file(self, filename, extension=''):
|
||||
return filename
|
||||
|
||||
def test_basic_serialization():
|
||||
widget = MockWWTWidget()
|
||||
test_state = widget._serialize_state('Title', 100, 200)
|
||||
assert test_state
|
||||
|
||||
assert 'wwt_settings' in test_state
|
||||
|
||||
assert 'html_settings' in test_state
|
||||
page_settings = test_state['html_settings']
|
||||
assert page_settings['title'] == 'Title'
|
||||
assert page_settings['max_width'] == 100
|
||||
assert page_settings['max_height'] == 200
|
||||
|
||||
assert 'view_settings' in test_state
|
||||
view_settings = test_state['view_settings']
|
||||
assert view_settings['mode'] == 'sky'
|
||||
assert view_settings['ra'] == pytest.approx(5.) # Behind the scenes unit conversion
|
||||
assert view_settings['dec'] == 10.
|
||||
assert view_settings['fov'] == 15.
|
||||
|
||||
assert 'foreground_settings' in test_state
|
||||
foreground_settings = test_state['foreground_settings']
|
||||
assert 'foreground' in foreground_settings
|
||||
assert 'background' in foreground_settings
|
||||
assert 'foreground_alpha' in foreground_settings
|
||||
|
||||
assert 'layers' in test_state
|
||||
assert test_state['layers'] == []
|
||||
|
||||
assert 'annotations' in test_state
|
||||
assert test_state['annotations'] == []
|
||||
|
||||
def test_widget_settings_serialization():
|
||||
widget = MockWWTWidget()
|
||||
widget.actual_planet_scale = True
|
||||
widget.alt_az_grid = False
|
||||
widget.constellation_boundaries = True
|
||||
widget.constellation_boundary_color = 'red'
|
||||
widget.constellation_figure_color = '#24680b'
|
||||
widget.constellation_figures = False
|
||||
widget.constellation_selection = True
|
||||
widget.constellation_selection_color = 'c' #cyan
|
||||
widget.crosshairs = True
|
||||
widget.crosshairs_color = (128./255.,64./255.,16./255.)
|
||||
widget.ecliptic = False
|
||||
widget.ecliptic_grid = True
|
||||
widget.galactic_grid = False
|
||||
widget.galactic_mode = True
|
||||
widget.grid = False
|
||||
widget.local_horizon_mode = True
|
||||
widget.location_altitude = 7*u.m
|
||||
widget.location_latitude = 12*u.deg
|
||||
widget.location_longitude = -18*u.deg
|
||||
expected_settings = {'actualPlanetScale': True, 'showAltAzGrid': False, 'showConstellationBoundries': True,
|
||||
'constellationBoundryColor': '#ff0000', 'constellationFigureColor': '#24680b',
|
||||
'showConstellationFigures': False, 'showConstellationSelection': True,
|
||||
'constellationSelectionColor': '#00bfbf', 'showCrosshairs': True, 'crosshairsColor': '#804010',
|
||||
'showEcliptic': False, 'showEclipticGrid': True, 'showGalacticGrid': False,
|
||||
'galacticMode': True, 'showGrid': False, 'localHorizonMode': True, 'locationAltitude': 7,
|
||||
'locationLat': 12., 'locationLng': -18.}
|
||||
state = widget.quick_serialize()
|
||||
assert state['wwt_settings'] == expected_settings
|
||||
|
||||
|
||||
def test_mode_serialization():
|
||||
view_mode_map = {'sky': 'sky', 'Sun': 'sun', 'Mercury': 'mercury', 'venus': 'venus', 'Earth': 'Bing Maps Aerial',
|
||||
'moon': 'moon', 'mars': 'Visible Imagery', 'jupiter': 'jupiter', 'callisto': 'callisto',
|
||||
'europa': 'europa', 'ganymede': 'ganymede', 'Io': 'io', 'saturn': 'saturn', 'Uranus': 'uranus',
|
||||
'neptune': 'neptune', 'Pluto': 'pluto', 'panorama': 'panorama',
|
||||
'Solar System': '3D Solar System View', 'milky way': '3D Solar System View',
|
||||
'universe': '3D Solar System View'}
|
||||
|
||||
widget = MockWWTWidget()
|
||||
for in_mode, out_mode in view_mode_map.items():
|
||||
widget.set_view(in_mode)
|
||||
assert widget.quick_serialize()['view_settings']['mode'] == out_mode, 'Mismatch for requested mode: {0}'.format(in_mode)
|
||||
|
||||
|
||||
def test_3d_serialization():
|
||||
widget = MockWWTWidget()
|
||||
widget.set_view('milky way')
|
||||
widget.solar_system.cosmos=True
|
||||
widget.solar_system.lighting=False
|
||||
widget.solar_system.milky_way = True
|
||||
widget.solar_system.minor_orbits = False
|
||||
widget.solar_system.orbits = True
|
||||
widget.solar_system.objects = False
|
||||
widget.solar_system.scale = 8
|
||||
widget.solar_system.stars = True
|
||||
|
||||
expected_3d_settings = {'solarSystemCosmos':True, 'solarSystemLighting':False, 'solarSystemMilkyWay':True,
|
||||
'solarSystemMinorOrbits':False, 'solarSystemOrbits':True, 'solarSystemPlanets':False,
|
||||
'solarSystemScale': '8', # The validation method casts the int to a string
|
||||
'solarSystemStars':True}
|
||||
|
||||
init_state = widget.quick_serialize()
|
||||
settings = init_state['wwt_settings']
|
||||
assert len(settings) == len(STARDARD_WWT_SETTINGS) + len(expected_3d_settings)
|
||||
for name, value in expected_3d_settings.items():
|
||||
assert name in settings
|
||||
assert value == settings[name], 'Mismatch for setting {0}'.format(name)
|
||||
|
||||
assert 'tracked_object_id' in init_state['view_settings']
|
||||
assert init_state['view_settings']['tracked_object_id'] == 0
|
||||
|
||||
track_id_map = {'sun': 0, 'mercury': 1, 'venus': 2, 'mars': 3, 'jupiter': 4, 'saturn': 5, 'uranus': 6, 'neptune': 7,
|
||||
'pluto': 8, 'moon': 9, 'io': 10, 'europa': 11, 'ganymede': 12, 'callisto': 13, 'ioshadow': 14,
|
||||
'europashadow': 15, 'ganymedeshadow': 16, 'callistoshadow': 17, 'suneclipsed': 18, 'earth': 19}
|
||||
|
||||
for obj_name, obj_id in track_id_map.items():
|
||||
widget.solar_system.track_object(obj_name)
|
||||
assert widget.quick_serialize()['view_settings']['tracked_object_id'] == obj_id, "ID mismatch for {0}".format(obj_name)
|
||||
|
||||
|
||||
def test_add_remove_annotation_serialization():
|
||||
widget = MockWWTWidget()
|
||||
circ = widget.add_circle()
|
||||
poly = widget.add_polygon()
|
||||
line = widget.add_line()
|
||||
state = widget.quick_serialize()
|
||||
assert len(state['annotations']) == 3
|
||||
for annotation in state['annotations']:
|
||||
assert annotation['id'] in [circ.id, poly.id, line.id]
|
||||
|
||||
circ.remove()
|
||||
state = widget.quick_serialize()
|
||||
assert len(state['annotations']) == 2
|
||||
for annotation in state['annotations']:
|
||||
assert annotation['id'] in [poly.id, line.id]
|
||||
|
||||
widget.clear_annotations()
|
||||
state = widget.quick_serialize()
|
||||
assert len(state['annotations']) == 0
|
||||
|
||||
|
||||
def test_circle_annotation_serialization():
|
||||
widget = MockWWTWidget()
|
||||
circ = widget.add_circle(fill_color='#012345', radius=0.3*u.deg)
|
||||
circ.set_center(SkyCoord(0.1*u.deg,0.2*u.deg))
|
||||
circ.fill = True
|
||||
circ.tag = 'Test Circ Tag'
|
||||
circ.line_color = 'orange'
|
||||
circ.line_width = 5*u.pix
|
||||
circ.opacity = 0.7
|
||||
circ.label = 'Test Circ Label'
|
||||
circ.hover_label = True
|
||||
expected_settings = {'radius': 0.3, 'fill': True, 'tag': 'Test Circ Tag', 'fillColor': '#012345', 'lineColor': '#ffa500',
|
||||
'lineWidth': 5, 'opacity': 0.7, 'label': 'Test Circ Label', 'showHoverLabel': True, 'skyRelative': True}
|
||||
|
||||
annot_state = widget.quick_serialize()['annotations'][0]
|
||||
|
||||
assert annot_state['id'] == circ.id
|
||||
assert annot_state['shape'] == 'circle'
|
||||
|
||||
assert 'center' in annot_state
|
||||
assert annot_state['center']['ra'] == pytest.approx(0.1)
|
||||
assert annot_state['center']['dec'] == pytest.approx(0.2)
|
||||
|
||||
assert 'settings' in annot_state
|
||||
assert annot_state['settings'] == expected_settings
|
||||
|
||||
circ.radius = 7*u.pix
|
||||
circ.fill_color = 'g'
|
||||
expected_settings['radius'] = 7
|
||||
expected_settings['skyRelative'] = False
|
||||
expected_settings['fillColor'] = '#008000'
|
||||
annot_state = widget.quick_serialize()['annotations'][0]
|
||||
assert annot_state['settings'] == expected_settings
|
||||
|
||||
#Check circle annotation with no specified center
|
||||
circ.remove()
|
||||
circ2 = widget.add_circle()
|
||||
center = widget.quick_serialize()['annotations'][0]['center']
|
||||
assert center['ra'] == pytest.approx(5.)
|
||||
assert center['dec'] == 10.
|
||||
|
||||
#Circle annotation with center in constructor
|
||||
circ2.remove()
|
||||
widget.add_circle(center = SkyCoord(15*u.deg,16*u.deg))
|
||||
center = widget.quick_serialize()['annotations'][0]['center']
|
||||
assert center['ra'] == 15
|
||||
assert center['dec'] == 16
|
||||
|
||||
|
||||
def test_poly_annotation_setting():
|
||||
widget = MockWWTWidget()
|
||||
poly = widget.add_polygon(fill = True, tag='Test Poly Tag')
|
||||
poly.fill_color = '#123456'
|
||||
poly.line_color = 'antiquewhite'
|
||||
poly.line_width = 9*u.pix
|
||||
poly.opacity = 0.9
|
||||
poly.label = 'Test Poly Label'
|
||||
poly.hover_label = False
|
||||
poly.add_point(SkyCoord([1,2,3]*u.deg,[5,6,7]*u.deg))
|
||||
poly.add_point(SkyCoord(4*u.deg,8*u.deg))
|
||||
expected_settings = {'fill': True, 'tag': 'Test Poly Tag', 'fillColor': '#123456', 'lineColor': '#faebd7',
|
||||
'lineWidth': 9, 'opacity': 0.9, 'label': 'Test Poly Label', 'showHoverLabel': False}
|
||||
|
||||
annot_state = widget.quick_serialize()['annotations'][0]
|
||||
|
||||
assert annot_state['id'] == poly.id
|
||||
assert annot_state['shape'] == 'polygon'
|
||||
|
||||
expected_ras = [1,2,3,4]
|
||||
expected_decs = [5,6,7,8]
|
||||
assert 'points' in annot_state
|
||||
pts = annot_state['points']
|
||||
assert len(pts) == 4
|
||||
for i in range(len(pts)):
|
||||
assert pts[i]['ra'] == expected_ras[i], 'RA mismatch for point {0}'.format(i)
|
||||
assert pts[i]['dec'] == expected_decs[i], 'Dec mismatch for point {0}'.format(i)
|
||||
|
||||
assert 'settings' in annot_state
|
||||
assert annot_state['settings'] == expected_settings
|
||||
|
||||
def test_line_annotation_setting():
|
||||
widget = MockWWTWidget()
|
||||
line = widget.add_line(color = '#abcde0')
|
||||
line.tag = 'Test Line Tag'
|
||||
line.width = 11*u.pix
|
||||
line.opacity = 0.2
|
||||
line.label = 'Test Line Label'
|
||||
line.hover_label = True
|
||||
line.add_point(SkyCoord([2,4,6]*u.deg,[10,12,14]*u.deg))
|
||||
line.add_point(SkyCoord(8*u.deg,16*u.deg))
|
||||
expected_settings = {'tag': 'Test Line Tag', 'lineColor': '#abcde0', 'lineWidth': 11, 'opacity': 0.2,
|
||||
'label': 'Test Line Label', 'showHoverLabel': True}
|
||||
|
||||
annot_state = widget.quick_serialize()['annotations'][0]
|
||||
|
||||
assert annot_state['id'] == line.id
|
||||
assert annot_state['shape'] == 'line'
|
||||
|
||||
expected_ras = [2,4,6,8]
|
||||
expected_decs = [10,12,14,16]
|
||||
assert 'points' in annot_state
|
||||
pts = annot_state['points']
|
||||
assert len(pts) == 4
|
||||
for i in range(len(pts)):
|
||||
assert pts[i]['ra'] == expected_ras[i], 'RA mismatch for point {0}'.format(i)
|
||||
assert pts[i]['dec'] == expected_decs[i], 'Dec mismatch for point {0}'.format(i)
|
||||
|
||||
assert 'settings' in annot_state
|
||||
assert annot_state['settings'] == expected_settings
|
||||
|
||||
|
||||
def test_add_remove_layer_serialization():
|
||||
widget = MockWWTWidget()
|
||||
|
||||
table = Table()
|
||||
table['flux'] = [2, 3, 4, 5, 6]
|
||||
table['dec'] = [84, 85, 86, 87, 88]
|
||||
table['ra'] = [250, 260, 270, 280, 290] * u.deg
|
||||
|
||||
table1 = widget.layers.add_table_layer(table=table)
|
||||
table2 = widget.layers.add_table_layer(table, color='#ff00ff')
|
||||
|
||||
array = np.arange(100).reshape((10, 10))
|
||||
wcs = WCS()
|
||||
wcs.wcs.ctype = 'GLON-CAR', 'GLAT-CAR'
|
||||
wcs.wcs.crpix = 50.5, 50.5
|
||||
wcs.wcs.cdelt = -0.03, 0.03
|
||||
wcs.wcs.crval = 33, 43
|
||||
img1 = widget.layers.add_image_layer(image=(array, wcs))
|
||||
wcs.wcs.crval = 33, 45
|
||||
img2 = widget.layers.add_image_layer((array, wcs))
|
||||
|
||||
state = widget.quick_serialize()
|
||||
|
||||
assert len(state['layers']) == 4
|
||||
layer_ids = [table1.id, table2.id, img1.id, img2.id]
|
||||
for layer in state['layers']:
|
||||
assert layer['id'] in layer_ids
|
||||
layer_ids.remove(layer['id'])
|
||||
|
||||
table2.remove()
|
||||
widget.layers.remove_layer(img2)
|
||||
state = widget.quick_serialize()
|
||||
|
||||
layer_ids = [table1.id,img1.id]
|
||||
assert len(state['layers']) == 2
|
||||
for layer in state['layers']:
|
||||
assert layer['id'] in layer_ids
|
||||
layer_ids.remove(layer['id'])
|
||||
|
||||
widget.reset()
|
||||
state = widget.quick_serialize()
|
||||
assert len(state['layers']) == 0
|
||||
|
||||
def test_table_setting_serialization():
|
||||
widget = MockWWTWidget()
|
||||
|
||||
table = Table()
|
||||
table['flux'] = [2, 3, 4, 5, 6]
|
||||
table['dec'] = [84, 85, 86, 87, 88]
|
||||
table['ra'] = [250, 260, 270, 280, 290]*u.deg
|
||||
|
||||
layer = widget.layers.add_table_layer(table, frame='earth', alt_att='flux', far_side_visible=True)
|
||||
layer.alt_type = 'distance'
|
||||
layer.color = '#aacc00'
|
||||
layer.size_scale = 14
|
||||
layer.opacity = 0.75
|
||||
layer.marker_type = 'square'
|
||||
layer.marker_scale = 'world'
|
||||
|
||||
layer_state = widget.quick_serialize()['layers'][0]
|
||||
assert layer_state['id'] == layer.id
|
||||
assert layer_state['layer_type'] == 'table'
|
||||
assert layer_state['frame'] == 'Earth'
|
||||
|
||||
assert 'settings' in layer_state
|
||||
expected_settings = {'lngColumn':'ra', 'raUnits': 'degrees', 'latColumn': 'dec', 'altColumn': 'flux',
|
||||
'altUnit': None, 'altType': 'distance', 'color': '#aacc00', 'scaleFactor': 14,
|
||||
'opacity': 0.75, 'plotType': 'square', 'markerScale': 'world', 'showFarSide': True,
|
||||
'sizeColumn': -1, '_colorMap': 0, 'colorMapColumn': -1}
|
||||
assert layer_state['settings'] == expected_settings
|
||||
|
||||
#Check when we have colormap and scaling
|
||||
layer.cmap_att = 'flux'
|
||||
layer.size_att = 'dec'
|
||||
layer.alt_unit = u.Mpc
|
||||
|
||||
expected_settings['sizeColumn'] = SIZE_COLUMN_NAME
|
||||
expected_settings['pointScaleType'] = 0
|
||||
expected_settings['colorMapColumn'] = CMAP_COLUMN_NAME
|
||||
expected_settings['_colorMap'] = 3
|
||||
expected_settings['altUnit'] = 'megaParsecs'
|
||||
|
||||
assert widget.quick_serialize()['layers'][0]['settings'] == expected_settings
|
||||
|
||||
def test_image_setting_serialization():
|
||||
widget = MockWWTWidget()
|
||||
|
||||
array = np.arange(100).reshape((10, 10))
|
||||
wcs = WCS()
|
||||
wcs.wcs.ctype = 'GLON-CAR', 'GLAT-CAR'
|
||||
wcs.wcs.crpix = 50.5, 50.5
|
||||
wcs.wcs.cdelt = -0.03, 0.03
|
||||
wcs.wcs.crval = 33, 43
|
||||
layer = widget.layers.add_image_layer(image=(array, wcs))
|
||||
|
||||
layer.opacity = 0.3
|
||||
layer.vmin = -1
|
||||
layer.vmax = 1
|
||||
layer_state = widget.quick_serialize()['layers'][0]
|
||||
|
||||
assert layer_state['id'] == layer.id
|
||||
assert layer_state['layer_type'] == 'image'
|
||||
assert 'stretch_info' in layer_state
|
||||
assert layer_state['stretch_info']['vmin'] == -1
|
||||
assert layer_state['stretch_info']['vmax'] == 1
|
||||
assert layer_state['stretch_info']['stretch'] == 0
|
||||
|
||||
assert 'settings' in layer_state
|
||||
settings = layer_state['settings']
|
||||
assert settings == {'opacity': 0.3}
|
||||
|
||||
stretches = {'linear': 0, 'log': 1, 'power': 2, 'sqrt': 3, 'histeq': 4}
|
||||
for stretch_name, stretch_id in stretches.items():
|
||||
layer.stretch = stretch_name
|
||||
assert widget.quick_serialize()['layers'][0]['stretch_info']['stretch'] == stretch_id, "Stretch id mismatch for: {0}".format(stretch_name)
|
Загрузка…
Ссылка в новой задаче