...
 
import math
from datetime import timedelta
from django.conf import settings
from django.utils.timezone import now, make_aware, utc
from network.base.models import Satellite, Station, Tle, Transmitter, Observation
from network.base.perms import schedule_perms
import ephem
......@@ -27,6 +29,14 @@ def get_azimuth(observer, satellite, date):
return float(format(math.degrees(satellite.az), '.0f'))
def over_station_horizon(elevation, station):
return elevation > station.horizon
def over_min_duration(duration):
return duration > settings.OBSERVATION_DURATION_MIN
def max_elevation_in_window(observer, satellite, pass_tca, window_start, window_end):
# In this case this is an overlapped observation
# re-calculate elevation and start/end azimuth
......@@ -116,7 +126,9 @@ def create_station_windows(station, existing_observations,
elevation = max_elevation_in_window(observer, satellite,
pass_params['tca_time'],
window_start, window_end)
if elevation < station.horizon:
window_duration = (window_end - window_start).total_seconds()
if not (over_station_horizon(elevation, station) and
over_min_duration(window_duration)):
continue
# Add a window for a partial pass
......@@ -168,21 +180,39 @@ def next_pass(observer, satellite):
'tca_alt': pass_elevation}
def predict_available_observation_windows(station, satellite, start_date, end_date, sat):
'''
Calculates available observation windows for a certain station and satellite during
def predict_available_observation_windows(station, min_horizon, tle, start_date, end_date, sat):
'''Calculate available observation windows for a certain station and satellite during
the given time period.
Returns list of passes found and list of available observation windows
:param station: Station for scheduling
:type station: Station django.db.model.Model
:param min_horizon: Overwrite station minimum horizon if defined
:type min_horizon: integer or None
:param tle: Satellite current TLE
:type tle: array of 3 strings
:param start_date: Start datetime of scheduling period
:type start_date: datetime string in '%Y-%m-%d %H:%M'
:param end_date: End datetime of scheduling period
:type end_date: datetime string in '%Y-%m-%d %H:%M'
:param sat: Satellite for scheduling
:type sat: Satellite django.db.model.Model
:return: List of passes found and list of available observation windows
'''
passes_found = []
# Initialize pyehem Satellite for propagation
satellite = ephem.readtle(*tle)
# Initialize pyephem Observer for propagation
observer = ephem.Observer()
observer.lon = str(station.lng)
observer.lat = str(station.lat)
observer.elevation = station.alt
observer.date = ephem.Date(start_date)
if min_horizon:
observer.horizon = str(min_horizon)
else:
observer.horizon = str(station.horizon)
satellite.compute(observer)
station_windows = []
while True:
......@@ -205,8 +235,10 @@ def predict_available_observation_windows(station, satellite, start_date, end_da
time_start_new = pass_params['set_time'] + timedelta(minutes=1)
observer.date = time_start_new.strftime("%Y-%m-%d %H:%M:%S.%f")
if pass_params['tca_alt'] < station.horizon:
# did not rise above user configured horizon
elevation = pass_params['tca_alt']
window_duration = (pass_params['set_time'] - pass_params['rise_time']).total_seconds()
if not (over_station_horizon(elevation, station) and
over_min_duration(window_duration)):
continue
# Check if overlaps with existing scheduled observations
......@@ -257,3 +289,23 @@ def create_new_observation(station_id,
rise_azimuth=rise_azimuth,
max_altitude=max_altitude,
set_azimuth=set_azimuth)
def get_available_stations(stations, downlink, user):
available_stations = []
for station in stations:
if not schedule_perms(user, station):
continue
# Skip if this station is not capable of receiving the frequency
if not downlink:
continue
frequency_supported = False
for gs_antenna in station.antenna.all():
if (gs_antenna.frequency <= downlink <= gs_antenna.frequency_max):
frequency_supported = True
if not frequency_supported:
continue
available_stations.append(station)
return available_stations
......@@ -16,12 +16,7 @@ base_urlpatterns = ([
url(r'^observations/(?P<id>[0-9]+)/delete/$', views.observation_delete,
name='observation_delete'),
url(r'^observations/new/$', views.observation_new, name='observation_new'),
url(r'^prediction_windows/(?P<sat_id>[\w.@+-]+)/(?P<transmitter>[\w.@+-]+)/'
r'(?P<start_date>.+)/(?P<end_date>.+)/(?P<station_id>[\w.@+-]+)/$',
views.prediction_windows, name='prediction_windows_filtered'),
url(r'^prediction_windows/(?P<sat_id>[\w.@+-]+)/(?P<transmitter>[\w.@+-]+)/'
r'(?P<start_date>.+)/(?P<end_date>.+)/$',
views.prediction_windows, name='prediction_windows'),
url(r'^prediction_windows/$', views.prediction_windows, name='prediction_windows'),
url(r'^pass_predictions/(?P<id>[\w.@+-]+)/$',
views.pass_predictions, name='pass_predictions'),
url(r'^observation_vet/(?P<id>[0-9]+)/(?P<status>[a-z]+)/$', views.observation_vet,
......@@ -35,6 +30,7 @@ base_urlpatterns = ([
url(r'^stations/edit/$', views.station_edit, name='station_edit'),
url(r'^stations/edit/(?P<id>[0-9]+)/$', views.station_edit, name='station_edit'),
url(r'^stations_all/$', views.StationAllView.as_view({'get': 'list'}), name='stations_all'),
url(r'^scheduling_stations/$', views.scheduling_stations, name='scheduling_stations'),
# Satellites
url(r'^satellites/(?P<id>[0-9]+)/$', views.satellite_view, name='satellite_view'),
......@@ -42,8 +38,5 @@ base_urlpatterns = ([
name='satellite_position'),
# Transmitters
url(r'^transmitters/(?P<id>[0-9]+)/$', views.transmitters_view,
name='transmitters_view'),
url(r'^transmitters/(?P<id>[0-9]+)/(?P<station_id>[\w.@+-]+)/$',
views.transmitters_view, name='transmitters_view_filtered'),
url(r'^transmitters/', views.transmitters_view, name='transmitters_view'),
], 'base')
......@@ -23,15 +23,23 @@ from network.users.models import User
from network.base.forms import StationForm, SatelliteFilterForm
from network.base.decorators import admin_required, ajax_required
from network.base.scheduling import (create_new_observation, ObservationOverlapError,
predict_available_observation_windows)
predict_available_observation_windows, get_available_stations)
from network.base.perms import schedule_perms, delete_perms, vet_perms
from network.base.tasks import update_all_tle, fetch_data
class StationSerializer(serializers.ModelSerializer):
status_display = serializers.SerializerMethodField()
class Meta:
model = Station
fields = ('name', 'lat', 'lng', 'id', 'status')
fields = ('name', 'lat', 'lng', 'id', 'status', 'status_display')
def get_status_display(self, obj):
try:
return obj.get_status_display()
except AttributeError:
return None
class StationAllView(viewsets.ReadOnlyModelViewSet):
......@@ -343,7 +351,6 @@ def observation_new(request):
satellites = Satellite.objects.filter(transmitters__alive=True) \
.filter(status='alive').distinct()
transmitters = Transmitter.objects.filter(alive=True)
obs_filter = {}
if request.method == 'GET':
......@@ -375,72 +382,70 @@ def observation_new(request):
obs_filter['exists'] = False
return render(request, 'base/observation_new.html',
{'satellites': satellites,
'transmitters': transmitters, 'obs_filter': obs_filter,
{'satellites': satellites, 'obs_filter': obs_filter,
'date_min_start': settings.OBSERVATION_DATE_MIN_START,
'date_min_end': settings.OBSERVATION_DATE_MIN_END,
'date_max_range': settings.OBSERVATION_DATE_MAX_RANGE})
@ajax_required
def prediction_windows(request, sat_id, transmitter, start_date, end_date,
station_id=None):
def prediction_windows(request):
sat_id = request.POST['satellite']
transmitter = request.POST['transmitter']
start_date = request.POST['start_time']
end_date = request.POST['end_time']
station_ids = request.POST.getlist('stations[]', [])
min_horizon = request.POST.get('min_horizon', None)
try:
sat = Satellite.objects.filter(transmitters__alive=True) \
.filter(status='alive').distinct().get(norad_cat_id=sat_id)
except Satellite.DoesNotExist:
data = {
data = [{
'error': 'You should select a Satellite first.'
}
}]
return JsonResponse(data, safe=False)
try:
tle = sat.latest_tle.str_array
except (ValueError, AttributeError):
data = {
data = [{
'error': 'No TLEs for this satellite yet.'
}
}]
return JsonResponse(data, safe=False)
try:
downlink = Transmitter.objects.get(uuid=transmitter).downlink_low
except Transmitter.DoesNotExist:
data = {
data = [{
'error': 'You should select a Transmitter first.'
}
}]
return JsonResponse(data, safe=False)
start_date = make_aware(datetime.strptime(start_date, '%Y-%m-%d %H:%M'), utc)
end_date = make_aware(datetime.strptime(end_date, '%Y-%m-%d %H:%M'), utc)
# Initialize pyehem Satellite for propagation
satellite = ephem.readtle(*tle)
data = []
stations = Station.objects.all()
if station_id:
stations = stations.filter(id=station_id)
stations = Station.objects.filter(status__gt=0)
if len(station_ids) > 0 and station_ids != ['']:
stations = stations.filter(id__in=station_ids)
if len(stations) == 0:
if len(station_ids) == 1:
data = [{
'error': 'Station is offline or it doesn\'t exist.'
}]
else:
data = [{
'error': 'Stations are offline or they don\'t exist.'
}]
return JsonResponse(data, safe=False)
passes_found = defaultdict(list)
stations_available = []
for station in stations:
if not schedule_perms(request.user, station):
continue
# Skip if this station is not capable of receiving the frequency
if not downlink:
continue
frequency_supported = False
for gs_antenna in station.antenna.all():
if (gs_antenna.frequency <= downlink <= gs_antenna.frequency_max):
frequency_supported = True
if not frequency_supported:
continue
stations_available.append(station.id)
available_stations = get_available_stations(stations, downlink, request.user)
for station in available_stations:
station_passes, station_windows = predict_available_observation_windows(station,
satellite,
min_horizon,
tle,
start_date,
end_date,
sat)
......@@ -458,12 +463,8 @@ def prediction_windows(request, sat_id, transmitter, start_date, end_date,
error_message = 'Satellite is always below horizon or ' \
'no free observation time available on visible stations.'
error_details = {}
for station in stations:
if station.id not in stations_available:
# Scheduling wasn't attempted (either due to missing permissions
# or missing receiver capability of the gs for the requested transmitter
pass
elif station.id not in passes_found.keys():
for station in available_stations:
if station.id not in passes_found.keys():
error_details[station.id] = 'Satellite is always above or below horizon.\n'
else:
error_details[station.id] = 'No free observation time during passes available.\n'
......@@ -611,6 +612,26 @@ def station_log(request, id):
{'station': station, 'station_log': station_log})
@ajax_required
def scheduling_stations(request):
"""Returns json with stations on which user has permissions to schedule"""
transmitter = request.POST.get('transmitter', None)
try:
downlink = Transmitter.objects.get(uuid=transmitter).downlink_low
except Transmitter.DoesNotExist:
data = [{
'error': 'You should select a Transmitter first.'
}]
return JsonResponse(data, safe=False)
stations = Station.objects.filter(status__gt=0)
available_stations = get_available_stations(stations, downlink, request.user)
data = {
'stations': StationSerializer(available_stations, many=True).data,
}
return JsonResponse(data, safe=False)
@ajax_required
def pass_predictions(request, id):
"""Endpoint for pass predictions"""
......@@ -819,16 +840,19 @@ def satellite_view(request, id):
return JsonResponse(data, safe=False)
def transmitters_view(request, id, station_id=None):
def transmitters_view(request):
sat_id = request.POST['satellite']
station_id = request.POST.get('station_id', None)
try:
sat = Satellite.objects.get(norad_cat_id=id)
sat = Satellite.objects.get(norad_cat_id=sat_id)
except Satellite.DoesNotExist:
data = {
'error': 'Unable to find that satellite.'
}
return JsonResponse(data, safe=False)
transmitters = Transmitter.objects.filter(satellite=sat, alive=True)
transmitters = Transmitter.objects.filter(satellite=sat, alive=True,
downlink_low__isnull=False)
transmitters_filtered = Transmitter.objects.none()
if station_id:
station = Station.objects.get(id=station_id)
......
......@@ -349,6 +349,8 @@ OBSERVATION_DATE_MIN_END = config('OBSERVATION_DATE_MIN_END', default=20, cast=i
OBSERVATION_DATE_MAX_RANGE = config('OBSERVATION_DATE_MAX_RANGE', default=2890, cast=int)
# Clean up threshold in days
OBSERVATION_OLD_RANGE = config('OBSERVATION_OLD_RANGE', default=30, cast=int)
# Minimum duration of observation in seconds
OBSERVATION_DURATION_MIN = config('OBSERVATION_DURATION_MIN', default=120, cast=int)
# Station settings
# Heartbeat for keeping a station online in minutes
......
......@@ -2,6 +2,11 @@
display: none;
}
#min-horizon-slider {
width: calc(100% - 20px);
margin: 10px 0 10px 10px;
}
.axis {
path,
......
This diff is collapsed.
......@@ -45,15 +45,17 @@
{% if obs_filter.norad %}
{% for satellite in satellites %}
{% ifequal satellite.norad_cat_id obs_filter.norad %}
<select id="satellite-selection" class="form-control selectpicker" name="satellite">
<select id="satellite-selection" class="form-control selectpicker" name="satellite" disabled>
<option data-norad="{{ satellite.norad_cat_id }}" value="{{ satellite.norad_cat_id }}" selected>
{{ satellite.norad_cat_id }} - {{ satellite.name }}
</option>
</select>
<input type="hidden" name="satellite" value="{{ satellite.norad_cat_id }}">
{% endifequal %}
{% endfor %}
{% else %}
<select id="satellite-selection" class="form-control selectpicker" name="satellite" data-live-search="true" autocomplete="off">
<select id="satellite-selection" class="form-control selectpicker" name="satellite"
data-live-search="true" autocomplete="off">
<option value="" selected>Select a satellite</option>
{% for satellite in satellites %}
<option data-norad="{{ satellite.norad_cat_id }}" value="{{ satellite.norad_cat_id }}">
......@@ -71,16 +73,15 @@
disabled name="transmitter" autocomplete="off">
<option id="no-transmitter" value="" selected>No transmitter available</option>
</select>
{% for satellite in satellites %}
<small class="tle" data-norad="{{ satellite.norad_cat_id }}">
{% if satellite.tle_no %}
Using TLE fetched
<span data-toggle="tooltip" data-placement="bottom" title="{{ satellite.tle_epoch|date:"Y-m-d H:i:s" }}">
{{ satellite.tle_epoch|naturaltime }}
</span>
{% endif %}
</small>
{% endfor %}
</div>
</div>
<div class="form-group">
<label class="col-sm-3 control-label">Stations</label>
<div class="col-sm-9">
<select id="station-selection" class="form-control selectpicker" data-live-search="true" data-count-selected-text="Selected {0} of {1} stations"
data-dropup-auto="false" multiple data-selected-text-format="count" data-actions-box="true" disabled name="station" autocomplete="off">
<option id="no-station" value="" selected>No station available</option>
</select>
</div>
</div>
</div>
......@@ -129,6 +130,32 @@
</div>
</div>
</div>
<div class="col-md-12">
<button type="button" id="advanced-options" class="btn btn-link" data-toggle="collapse"
data-target=".collapse-option" aria-expanded="false" aria-controls="collapse-option">
<span class="glyphicon glyphicon-chevron-down"></span> Show Advanced Options
</button>
</div>
<div class="col-md-6 collapse collapse-option">
<div class="form-group">
<label class="col-sm-3 control-label">Station Horizon</label>
<div class="col-sm-9" id="horizon-status">
<div class="btn-group btn-group-justified" data-toggle="buttons">
<label class="btn btn-default active" id="default-horizon">
<input type="radio" name="horizon" value="default" autocomplete="off" checked>
Minimum of each Station
</input>
</label>
<label class="btn btn-default" id="custom-horizon">
<input type="radio" name="horizon" value="custom" autocomplete="off">
Custom
</input>
</label>
</div>
<input type="hidden" id="min-horizon" disabled/>
</div>
</div>
</div>
</div>
{% if obs_filter.ground_station %}
......