Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/basic finder chart #263

Merged
merged 11 commits into from
Mar 30, 2020
25 changes: 23 additions & 2 deletions tom_observations/facilities/lt.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from crispy_forms.layout import Layout, HTML

from tom_observations.facility import GenericObservationForm
from tom_observations.facility import GenericObservationFacility, GenericObservationForm


class LTQueryForm(GenericObservationForm):
Expand All @@ -19,9 +19,30 @@ def __init__(self, *args, **kwargs):
)


class LTFacility():
class LTFacility(GenericObservationFacility):
name = 'LT'
observation_types = [('Default', '')]

def get_form(self, observation_type):
return LTQueryForm

def submit_observation(self, observation_payload):
return

def validate_observation(self, observation_payload):
return

def get_observation_url(self, observation_id):
return

def get_terminal_observing_states(self):
return []

def get_observing_sites(self):
return {}

def get_observation_status(self, observation_id):
return

def data_products(self, observation_id, product_id=None):
return []
22 changes: 18 additions & 4 deletions tom_observations/templates/tom_observations/observation_form.html
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
{% extends 'tom_common/base.html' %}
{% load bootstrap4 crispy_forms_tags observation_extras %}
{% load bootstrap4 crispy_forms_tags observation_extras targets_extras %}
{% block title %}Submit Observation{% endblock %}
{% block content %}
<h3>Submit an observation to {{ form.facility.value }}</h3>
{% observation_type_tabs %}
{% crispy form %}
{% endblock %}
{% if target.type == 'SIDEREAL' %}
<div class="row">
<div class="col">
{% observation_plan target form.facility.value %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-4">
{% target_data target %}
</div>
<div class="col-md-8">
{% observation_type_tabs %}
{% crispy form %}
</div>
</div>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{% load bootstrap4 %}
<div id="plan-panel">
{{ visibility_graph|safe }}
</div>
39 changes: 30 additions & 9 deletions tom_observations/templatetags/observation_extras.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from datetime import datetime, timedelta
from urllib.parse import urlencode

from django import forms, template
Expand All @@ -9,6 +10,7 @@
from tom_observations.models import ObservationRecord
from tom_observations.facility import get_service_classes
from tom_observations.observing_strategy import RunStrategyForm
from tom_observations.utils import get_sidereal_visibility
from tom_targets.models import Target


Expand Down Expand Up @@ -41,19 +43,38 @@ def observation_type_tabs(context):
}


@register.inclusion_tag('tom_observations/partials/observation_list.html', takes_context=True)
def observation_list(context, target=None):
@register.inclusion_tag('tom_observations/partials/observation_plan.html')
def observation_plan(target, facility, length=7, interval=60, airmass_limit=None):
"""
Displays form and renders plot for visibility calculation. Using this templatetag to render a plot requires that
the context of the parent view have values for start_time, end_time, and airmass.
"""

visibility_graph = ''
start_time = datetime.now()
end_time = start_time + timedelta(days=length)

visibility_data = get_sidereal_visibility(target, start_time, end_time, interval, airmass_limit)
plot_data = [
go.Scatter(x=data[0], y=data[1], mode='lines', name=site) for site, data in visibility_data.items()
]
layout = go.Layout(yaxis=dict(autorange='reversed'))
visibility_graph = offline.plot(
go.Figure(data=plot_data, layout=layout), output_type='div', show_link=False
)

return {
'visibility_graph': visibility_graph
}


@register.inclusion_tag('tom_observations/partials/observation_list.html')
def observation_list(target=None):
"""
Displays a list of all observations in the TOM, limited to an individual target if specified.
"""
if target:
if settings.TARGET_PERMISSIONS_ONLY:
observations = target.observationrecord_set.all()
else:
observations = get_objects_for_user(
context['request'].user,
'tom_observations.view_observationrecord'
).filter(target=target)
observations = target.observationrecord_set.all()
else:
observations = ObservationRecord.objects.all().order_by('-created')
return {'observations': observations}
Expand Down
2 changes: 2 additions & 0 deletions tom_observations/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,8 @@ def get_context_data(self, **kwargs):
"""
context = super(ObservationCreateView, self).get_context_data(**kwargs)
context['type_choices'] = self.get_facility_class().observation_types
target = Target.objects.get(pk=self.get_target_id())
context['target'] = target
return context

def get_form_class(self):
Expand Down
2 changes: 1 addition & 1 deletion tom_setup/templates/tom_setup/settings.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ TOM_LATEX_PROCESSORS = {
TOM_FACILITY_CLASSES = [
'tom_observations.facilities.lco.LCOFacility',
'tom_observations.facilities.gemini.GEMFacility',
'tom_observations.facilities.soar.SOARFacility
'tom_observations.facilities.soar.SOARFacility',
'tom_observations.facilities.lt.LTFacility'
]

Expand Down
172 changes: 166 additions & 6 deletions tom_targets/templates/tom_targets/partials/aladin.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,176 @@
<!-- insert this snippet where you want Aladin Lite viewer to appear and after the loading of jQuery -->
<h3>Survey View</h3>
<div id="aladin-lite-div" style="width:300px;height:300px;"></div>
<div id="chart-form-div" style="width:300px;">
<form id="chart-form">
<div class="form-group mt-1 mb-1">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text bg-transparent" style="font-family: inherit;">Field of view</span>
</div>
<input type="number" class="form-control" aria-label="Field of view" id="fov" min="0" value="10">
<div class="input-group-append">
<select id="fov-units-select" class="form-control">
<option>arcsec</option>
<option selected>arcmin</option>
<option>deg</option>
</select>
</div>
</div>
</div>
<div class="form-group mt-1 mb-1">
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text bg-transparent" style="font-family: inherit;" for="scale-bar-units-select">Scale bar</label>
</div>
<input type="number" class="form-control" aria-label="Scale bar size" id="scale-bar-size" min="0" value="1">
<div class="input-group-append">
<select id="scale-bar-units-select" class="form-control">
<option>arcsec</option>
<option selected>arcmin</option>
<option>deg</option>
</select>
</div>
</div>
</div>
<div class="form-group mt-1 mb-1">
<input class="btn btn-primary" type="button" onclick="updateFromForm({{ target.ra }}, {{ target.dec }})" value="Update">
<a class="btn btn-primary" id="download-chart" href="" download="chart.png" onclick="downloadImage()">Save Image</a>
</div>
</form>
</div>
<script type="text/javascript" src="//aladin.u-strasbg.fr/AladinLite/api/v2/latest/aladin.min.js" charset="utf-8"></script>
<script type="text/javascript">
var aladin = A.aladin('#aladin-lite-div', {

let aladin = A.aladin('#aladin-lite-div', {
survey: "P/DSS2/color",
fov:5,
fov: getFovAsDegreesFromForm(),
showReticle: false,
target: "{{ target.ra }} {{ target.dec }}",
showGotoControl: false,
showZoomControl: false
});
var target_cat = A.catalog({name: '{{ target.name }}', sourceSize: 10, color: 'red'});
aladin.addCatalog(target_cat, {shape: 'circle'});
target_cat.addSources([A.marker({{ target.ra }}, {{ target.dec }},
{popupTitle: '{{ target.name }}'})]);

aladin.on('positionChanged', function() {
annotateChart({{ target.ra }}, {{ target.dec }});
});

aladin.on('zoomChanged', function() {
annotateChart({{ target.ra }}, {{ target.dec }});
});

function getScaleBarFromForm() {
let size = Number($('#scale-bar-size').val());
if (size < 0) {
size = 0;
}
const units = $('#scale-bar-units-select option:selected').val();
const label = String(size) + ' ' + units;
const sizeAsDegrees = toDegrees(size, units);
return {size: size, units: units, label: label, sizeAsDegrees: sizeAsDegrees};
}

function getFovAsDegreesFromForm() {
const fov = Number($('#fov').val());
const units = $('#fov-units-select option:selected').val();
let fovAsDegrees;
if (fov >= 0) {
fovAsDegrees = toDegrees(fov, units);
}
return fovAsDegrees;
}

function toDegrees(value, units) {
if (units === 'arcmin') {
return value / 60;
} else if (units === 'arcsec') {
return value / 3600;
} else {
return value;
}
}

function annotateChart(targetRa, targetDec) {
const fovDegrees = aladin.getFov()[0];
const scaleBar = getScaleBarFromForm();
// Pixel position (0,0) is the top left corner of the view
const viewSizePix = aladin.getSize();
const offsetPixFromEdge = 30;
const scaleBarStartPix = [offsetPixFromEdge, viewSizePix[1] - offsetPixFromEdge]; // Bottom left corner
const compassCenterPix = [viewSizePix[0] - offsetPixFromEdge, viewSizePix[1] - offsetPixFromEdge]; // Bottom right corner
// Compass position
const cosDec = Math.cos(targetDec * Math.PI / 180);
const compassArmLength = fovDegrees / 10;
const compassCenter = aladin.pix2world(compassCenterPix[0], compassCenterPix[1]);
const compassNorthArm = [compassCenter[0], compassCenter[1] + compassArmLength];
const compassNorthArmPix = aladin.world2pix(compassNorthArm[0], compassNorthArm[1]);
const compassEastArm = [compassCenter[0] + compassArmLength / cosDec, compassCenter[1]];
const compassEastArmPix = aladin.world2pix(compassEastArm[0], compassEastArm[1]);
// Scale bar position
const scaleBarStart = aladin.pix2world(scaleBarStartPix[0], scaleBarStartPix[1]);
const scaleBarEnd = [scaleBarStart[0] - scaleBar.sizeAsDegrees / cosDec, scaleBarStart[1]];
const scaleBarEndPix = aladin.world2pix(scaleBarEnd[0], scaleBarEnd[1]);
const scaleBarLength = Math.abs(scaleBarEndPix[0] - scaleBarStartPix[0]);
// Re-draw the annotations on the chart
const color = '#f72525';
const scaleBarTextSpacing = 7;
const compassTextSpacing = 3;
aladin.removeLayers();
let layer = A.graphicOverlay({name: 'chart annotations', color: color, lineWidth: 2});
aladin.addOverlay(layer);
layer.add(A.polyline([compassNorthArm, compassCenter, compassEastArm]));
layer.add(A.polyline([scaleBarStart, scaleBarEnd]));
layer.add(A.circle(targetRa, targetDec, fovDegrees / 30));
layer.add(new Text(scaleBarStartPix[0] + scaleBarLength / 2, scaleBarStartPix[1] - scaleBarTextSpacing, scaleBar.label, {color: color}));
layer.add(new Text(compassNorthArmPix[0], compassNorthArmPix[1] - compassTextSpacing, 'N', {color: color}));
layer.add(new Text(compassEastArmPix[0] - compassTextSpacing, compassEastArmPix[1], 'E', {color: color, align: 'end', baseline: 'middle'}));
}

function downloadImage() {
// Update the data that the link that was clicked will download
$('#download-chart').attr('href', aladin.getViewDataURL());
return true;
}

function updateFromForm(ra, dec) {
const fov = getFovAsDegreesFromForm();
if (fov !== undefined) {
aladin.setFov(fov);
annotateChart(ra, dec);
}
}

Text = (function() {
// The AladinLite API does not provide a way to draw arbitrary text at an arbitrary location in an overlay layer.
// This implements the methods necessary to do so when provided as an input to layer.add(). This approach was
// preferable to the others (possibilities included directly getting and drawing on the actual canvas element that the
// other overlay elements are drawn on, or creating another canvas element and placing it directly on top of
// the others) as the text that is drawn will then be integrated with the draw/destroy/redraw loops within aladin,
// and the text will show up in the generated data url that is used for saving an image without having to do anything extra.

Text = function(x, y, text, options) {
options = options || {};
this.x = x || undefined;
this.y = y || undefined;
this.text = text || '';
this.color = options['color'] || undefined;
this.align = options['align'] || 'center';
this.baseline = options['baseline'] || 'alphabetic';
this.overlay = null;
};

Text.prototype.setOverlay = function(overlay) {
this.overlay = overlay;
};

Text.prototype.draw = function(ctx) {
ctx.fillStyle = this.color;
ctx.font = '15px Arial';
ctx.textAlign = this.align;
ctx.textBaseline = this.baseline;
ctx.fillText(this.text, this.x, this.y);
};

return Text;
})();
</script>
3 changes: 3 additions & 0 deletions tom_targets/templates/tom_targets/partials/moon_distance.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<div id="lunar-plot">
{{ plot|safe }}
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<a href="{% url 'tom_targets:update' pk=target.id %}" title="Update target" class="btn btn-primary">Update Target</a>
<a href="{% url 'tom_targets:delete' pk=target.id %}" title="Delete target" class="btn btn-warning">Delete Target</a>
2 changes: 0 additions & 2 deletions tom_targets/templates/tom_targets/partials/target_data.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
{% load tom_common_extras targets_extras %}
<a href="{% url 'tom_targets:update' pk=target.id %}" title="Update target" class="btn btn-primary">Update Target</a>
<a href="{% url 'tom_targets:delete' pk=target.id %}" title="Delete target" class="btn btn-warning">Delete Target</a>
<dl class="row">
{% for target_name in target.names %}
{% if forloop.first %}
Expand Down
8 changes: 6 additions & 2 deletions tom_targets/templates/tom_targets/target_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@
{{ object.future_observations|length }} upcoming observation{{ object.future_observations|pluralize }}
</div>
{% endif %}
{% target_buttons object %}
{% target_data object %}
{% if object.type == 'SIDEREAL' %}
{% aladin object %}
{% endif %}
</div>
</div>
<div class="col-md-8">
Expand Down Expand Up @@ -49,9 +52,10 @@ <h4>Observe</h4>
<hr/>
<h4>Plan</h4>
{% if object.type == 'SIDEREAL' %}
{% target_plan %}
{% target_plan %}
{% moon_distance object %}
{% elif target.type == 'NON_SIDEREAL' %}
<p>Airmass plotting for non-sidereal targets is not currently supported. If you would like to add this functionality, please check out the <a href="https://github.com/TOMToolkit/tom_nonsidereal_airmass" target="_blank">non-sidereal airmass plugin.</a></p>
<p>Airmass plotting for non-sidereal targets is not currently supported. If you would like to add this functionality, please check out the <a href="https://github.com/TOMToolkit/tom_nonsidereal_airmass" target="_blank">non-sidereal airmass plugin.</a></p>
{% endif %}
</div>
<div class="tab-pane" id="observations">
Expand Down
Loading