-
Notifications
You must be signed in to change notification settings - Fork 115
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
This renders aircraft from flightradar24 much like the mode_s plugin.
- Loading branch information
1 parent
6fe5ba9
commit 9a19982
Showing
3 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,267 @@ | ||
# Copyright 2013, 2014, 2015, 2016, 2017, 2018 Kevin Reid and the ShinySDR contributors | ||
# | ||
# This file is part of ShinySDR. | ||
# | ||
# ShinySDR is free software: you can redistribute it and/or modify | ||
# it under the terms of the GNU General Public License as published by | ||
# the Free Software Foundation, either version 3 of the License, or | ||
# (at your option) any later version. | ||
# | ||
# ShinySDR is distributed in the hope that it will be useful, | ||
# but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
# GNU General Public License for more details. | ||
# | ||
# You should have received a copy of the GNU General Public License | ||
# along with ShinySDR. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
# pylint: disable=maybe-no-member, no-member | ||
# (maybe-no-member: GR swig) | ||
# (no-member: Twisted reactor) | ||
|
||
from __future__ import absolute_import, division, print_function, unicode_literals | ||
|
||
import json | ||
import os.path | ||
|
||
import six | ||
|
||
from twisted.internet import task | ||
from twisted.python.url import URL | ||
from twisted.web import static | ||
from twisted.web.client import Agent, readBody | ||
from zope.interface import Interface, implementer | ||
|
||
from shinysdr.devices import Device, IComponent, ITopWatcher | ||
from shinysdr.interfaces import ClientResourceDef | ||
from shinysdr.telemetry import ITelemetryMessage, ITelemetryObject, TelemetryItem, Track, empty_track | ||
from shinysdr.types import TimestampT | ||
from shinysdr.values import ExportedState, exported_value, setter | ||
|
||
_POLLING_INTERVAL = 8 | ||
drop_unheard_timeout_seconds = 60 | ||
|
||
|
||
_SECONDS_PER_HOUR = 60 * 60 | ||
_METERS_PER_NAUTICAL_MILE = 1852 | ||
_KNOTS_TO_METERS_PER_SECOND = _METERS_PER_NAUTICAL_MILE / _SECONDS_PER_HOUR | ||
_CM_PER_INCH = 2.54 | ||
_INCH_PER_FOOT = 12 | ||
_METERS_PER_FEET = (_CM_PER_INCH * _INCH_PER_FOOT) / 100 | ||
_FEET_PER_MINUTE_TO_METERS_PER_SECOND = _METERS_PER_FEET * 60 | ||
|
||
|
||
# TODO: This really shouldn't be a Device, but that's the only way right now to hook into the config. | ||
def Flightradar24(reactor, key='flightradar24', bounds=None): | ||
"""Create a flightradar24 client. | ||
key: Component ID. | ||
bounds: optional 4-element tuple of (lat1, lat2, lon1, lon2) to restrict search | ||
""" | ||
return Device(components={six.text_type(key): _Flightradar24Client( | ||
reactor=reactor, | ||
bounds=bounds)}) | ||
|
||
|
||
@implementer(IComponent, ITopWatcher) | ||
class _Flightradar24Client(ExportedState): | ||
def __init__(self, reactor, bounds): | ||
self.__reactor = reactor | ||
self.__agent = Agent(reactor) | ||
self.__bounds = bounds | ||
self.__top = None | ||
self.__loop = None | ||
|
||
@exported_value(type=bool, changes='this_setter', label='Enabled') | ||
def get_enabled(self): | ||
return self.__loop is not None | ||
|
||
@setter | ||
def set_enabled(self, enabled): | ||
if enabled and not self.__loop: | ||
self.__loop = task.LoopingCall(self.__send_request) | ||
self.__loop.clock = self.__reactor | ||
self.__loop.start(_POLLING_INTERVAL).addErrback(print) | ||
elif not enabled and self.__loop: | ||
self.__loop.stop() | ||
self.__loop = None | ||
|
||
def close(self): | ||
if self.__loop: | ||
self.__loop.stop() | ||
self.__loop = None | ||
|
||
def set_top(self, top): | ||
self.__top = top | ||
|
||
def __make_url(self): | ||
u = URL.fromText('https://data-live.flightradar24.com/zones/fcgi/feed.js?faa=1&mlat=1&flarm=1&adsb=1&gnd=0&air=1&vehicles=0&estimated=1&maxage=14400&gliders=1&stats=0') | ||
if self.__bounds: | ||
u = u.set('bounds', ','.join(str(b) for b in self.__bounds)) | ||
return six.binary_type(u.asText()) | ||
|
||
def __send_request(self): | ||
if not self.__top: | ||
return | ||
d = self.__agent.request(six.binary_type('GET'), self.__make_url()) | ||
d.addCallback(readBody) | ||
|
||
def process(body): | ||
data = json.loads(body) | ||
for object_id, aircraft in six.iteritems(data): | ||
if not isinstance(aircraft, list): | ||
continue | ||
self.__top.get_telemetry_store().receive(AircraftWrapper(object_id, aircraft)) | ||
d.addCallback(process) | ||
d.addErrback(print) | ||
|
||
|
||
@implementer(ITelemetryMessage) | ||
class AircraftWrapper(object): | ||
def __init__(self, object_id, message): | ||
self.object_id = object_id | ||
self.message = message # list | ||
|
||
def get_object_id(self): | ||
# TODO: add prefix to ensure uniqueness? | ||
return self.object_id | ||
|
||
def get_object_constructor(self): | ||
return Aircraft | ||
|
||
|
||
class IAircraft(Interface): | ||
"""marker interface for client""" | ||
pass | ||
|
||
|
||
@implementer(IAircraft, ITelemetryObject) | ||
class Aircraft(ExportedState): | ||
def __init__(self, object_id): | ||
"""Implements ITelemetryObject. object_id is the hex formatted address.""" | ||
self.__last_heard_time = None | ||
self.__track = empty_track | ||
self.__callsign = None | ||
self.__registration = None | ||
self.__origin = None | ||
self.__destination = None | ||
self.__flight = None | ||
self.__squawk_code = None | ||
self.__model = None | ||
|
||
# not exported | ||
def receive(self, message_wrapper): | ||
d = message_wrapper.message | ||
# Fields from https://github.com/derhuerst/flightradar24-client/blob/master/lib/radar.js | ||
|
||
timestamp = d[10] | ||
|
||
# Part of self.__track | ||
latitude = d[1] | ||
longitude = d[2] | ||
altitude = d[4] # in feet | ||
bearing = d[3] # in degrees | ||
speed = d[5] # in knots | ||
rate_of_climb = d[15] # ft/min | ||
|
||
# Shown separately | ||
callsign = d[16] # ICAO ATC call signature | ||
registration = d[9] | ||
origin = d[11] # airport IATA code | ||
destination = d[12] # airport IATA code | ||
flight = d[13] | ||
squawk_code = d[6] # https://en.wikipedia.org/wiki/Transponder_(aeronautics) | ||
model = d[8] # ICAO aircraft type designator | ||
|
||
# Unused | ||
#is_on_ground = bool(d[14]) | ||
#mode_s_code = d[0] # // ICAO aircraft registration number | ||
#radar = d[7] # F24 "radar" data source ID | ||
#is_glider = bool(d[17]) | ||
|
||
new = {} | ||
if latitude and longitude: | ||
new.update( | ||
latitude=TelemetryItem(latitude, timestamp), | ||
longitude=TelemetryItem(longitude, timestamp), | ||
) | ||
if altitude: | ||
new.update(altitude=TelemetryItem(altitude * _METERS_PER_FEET, timestamp)) | ||
if speed: | ||
new.update(h_speed=TelemetryItem(speed * _KNOTS_TO_METERS_PER_SECOND, timestamp)) | ||
if bearing: | ||
new.update( | ||
heading=TelemetryItem(bearing, timestamp), | ||
track_angle=TelemetryItem(bearing, timestamp), | ||
) | ||
if rate_of_climb: | ||
new.update(v_speed=TelemetryItem(rate_of_climb * _FEET_PER_MINUTE_TO_METERS_PER_SECOND, timestamp)) | ||
if new: | ||
self.__track = self.__track._replace(**new) | ||
|
||
self.__last_heard_time = timestamp | ||
self.__callsign = callsign | ||
self.__registration = registration | ||
self.__origin = origin | ||
self.__destination = destination | ||
self.__flight = flight | ||
self.__squawk_code = squawk_code | ||
self.__model = model | ||
self.state_changed() | ||
|
||
def is_interesting(self): | ||
""" | ||
Implements ITelemetryObject. Does this aircraft have enough information to be worth mentioning? | ||
""" | ||
# TODO: Loosen this rule once we have more efficient state transfer (no polling) and better UI for viewing them on the client. | ||
return \ | ||
self.__track.latitude.value is not None or \ | ||
self.__track.longitude.value is not None or \ | ||
self.__call is not None or \ | ||
self.__aircraft_type is not None | ||
|
||
def get_object_expiry(self): | ||
"""implement ITelemetryObject""" | ||
return self.__last_heard_time + drop_unheard_timeout_seconds | ||
|
||
@exported_value(type=TimestampT(), changes='explicit', sort_key='100', label='Last heard') | ||
def get_last_heard_time(self): | ||
return self.__last_heard_time | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='020', label='Callsign') | ||
def get_callsign(self): | ||
return self.__callsign | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='030', label='Registration') | ||
def get_registration(self): | ||
return self.__registration | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='040', label='Origin') | ||
def get_origin(self): | ||
return self.__origin | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='050', label='Destination') | ||
def get_destination(self): | ||
return self.__destination | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='060', label='Flight') | ||
def get_flight(self): | ||
return self.__flight | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='070', label='Squawk Code') | ||
def get_squawk_code(self): | ||
return self.__squawk_code | ||
|
||
@exported_value(type=six.text_type, changes='explicit', sort_key='080', label='Model') | ||
def get_model(self): | ||
return self.__model | ||
|
||
@exported_value(type=Track, changes='explicit', sort_key='010', label='') | ||
def get_track(self): | ||
return self.__track | ||
|
||
|
||
plugin_client = ClientResourceDef( | ||
key=__name__, | ||
resource=static.File(os.path.join(os.path.split(__file__)[0], 'client')), | ||
load_js_path='flightradar24.js') |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
// Copyright 2014, 2015, 2016, 2017 Kevin Reid and the ShinySDR contributors | ||
// | ||
// This file is part of ShinySDR. | ||
// | ||
// ShinySDR is free software: you can redistribute it and/or modify | ||
// it under the terms of the GNU General Public License as published by | ||
// the Free Software Foundation, either version 3 of the License, or | ||
// (at your option) any later version. | ||
// | ||
// ShinySDR is distributed in the hope that it will be useful, | ||
// but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
// GNU General Public License for more details. | ||
// | ||
// You should have received a copy of the GNU General Public License | ||
// along with ShinySDR. If not, see <http://www.gnu.org/licenses/>. | ||
|
||
'use strict'; | ||
|
||
define([ | ||
'require', | ||
'map/map-core', | ||
'widgets', | ||
'widgets/basic', | ||
], ( | ||
require, | ||
import_map_core, | ||
widgets, | ||
import_widgets_basic | ||
) => { | ||
const { | ||
register, | ||
renderTrackFeature, | ||
} = import_map_core; | ||
const { | ||
Block, | ||
} = import_widgets_basic; | ||
|
||
const exports = {}; | ||
|
||
function AircraftWidget(config) { | ||
Block.call(this, config, function (block, addWidget, ignore, setInsertion, setToDetails, getAppend) { | ||
addWidget('track', widgets.TrackWidget); | ||
// TODO: More compact rendering of cells. | ||
}, false); | ||
} | ||
|
||
// TODO: Better widget-plugin system so we're not modifying should-be-static tables | ||
widgets['interface:shinysdr.plugins.flightradar24.IAircraft'] = AircraftWidget; | ||
|
||
function addAircraftMapLayer(mapPluginConfig) { | ||
mapPluginConfig.addLayer('flightradar24', { | ||
featuresCell: mapPluginConfig.index.implementing('shinysdr.plugins.flightradar24.IAircraft'), | ||
featureRenderer: function renderAircraft(aircraft, dirty) { | ||
var trackCell = aircraft.track; | ||
var callsign = aircraft.callsign.depend(dirty); | ||
var ident = aircraft.squawk_code.depend(dirty); | ||
var altitude = trackCell.depend(dirty).altitude.value; | ||
var labelParts = []; | ||
if (callsign !== null) { | ||
labelParts.push(callsign.replace(/^ | $/g, '')); | ||
} | ||
if (ident !== null) { | ||
labelParts.push(ident); | ||
} | ||
if (altitude !== null) { | ||
labelParts.push(altitude.toFixed(0) + ' m'); | ||
} | ||
var f = renderTrackFeature(dirty, trackCell, | ||
labelParts.join(' • ')); | ||
f.iconURL = require.toUrl('./aircraft.svg'); | ||
return f; | ||
} | ||
}); | ||
} | ||
|
||
register(addAircraftMapLayer); | ||
|
||
return Object.freeze(exports); | ||
}); |