Merge pull request #215 from jsub1/interactive-figure

Interactive Figure Proof of Concept
This commit is contained in:
Peter Williams 2019-06-27 12:54:51 -04:00 коммит произвёл GitHub
Родитель 871031f530 8e07376fdc
Коммит fa54ee6815
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 992 добавлений и 35 удалений

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

@ -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):

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

@ -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)