Skip to content

Commit

Permalink
Add flightradar24 client.
Browse files Browse the repository at this point in the history
This renders aircraft from flightradar24 much like the mode_s plugin.
  • Loading branch information
quentinmit committed Dec 10, 2018
1 parent 6fe5ba9 commit 9a19982
Show file tree
Hide file tree
Showing 3 changed files with 384 additions and 0 deletions.
267 changes: 267 additions & 0 deletions shinysdr/plugins/flightradar24/__init__.py
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')
37 changes: 37 additions & 0 deletions shinysdr/plugins/flightradar24/client/aircraft.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
80 changes: 80 additions & 0 deletions shinysdr/plugins/flightradar24/client/flightradar24.js
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);
});

0 comments on commit 9a19982

Please sign in to comment.