e5053ac5c9
Otherwise, if the position data times out on the dump1090 side, we'd keep the last seen position on the webpage side but lose the mlat status.
425 lines
16 KiB
JavaScript
425 lines
16 KiB
JavaScript
"use strict";
|
|
|
|
function PlaneObject(icao) {
|
|
// Info about the plane
|
|
this.icao = icao;
|
|
this.flight = null;
|
|
this.squawk = null;
|
|
this.selected = false;
|
|
this.category = null;
|
|
|
|
// Basic location information
|
|
this.altitude = null;
|
|
this.speed = null;
|
|
this.track = null;
|
|
this.position = null;
|
|
this.position_from_mlat = false
|
|
this.sitedist = null;
|
|
|
|
// Data packet numbers
|
|
this.messages = null;
|
|
this.rssi = null;
|
|
|
|
// Track history as a series of line segments
|
|
this.track_linesegs = [];
|
|
this.history_size = 0;
|
|
|
|
// When was this last updated (receiver timestamp)
|
|
this.last_message_time = null;
|
|
this.last_position_time = null;
|
|
|
|
// When was this last updated (seconds before last update)
|
|
this.seen = null;
|
|
this.seen_pos = null;
|
|
|
|
// Display info
|
|
this.visible = true;
|
|
this.marker = null;
|
|
this.icon = { type: 'generic',
|
|
fillOpacity: 0.9 };
|
|
|
|
// request metadata
|
|
this.registration = null;
|
|
this.icaotype = null;
|
|
getAircraftData(this.icao).done(function(data) {
|
|
if ("r" in data) {
|
|
this.registration = data.r;
|
|
}
|
|
|
|
if ("t" in data) {
|
|
this.icaotype = data.t;
|
|
}
|
|
|
|
if (this.selected) {
|
|
refreshSelected();
|
|
}
|
|
}.bind(this));
|
|
}
|
|
|
|
// Appends data to the running track so we can get a visual tail on the plane
|
|
// Only useful for a long running browser session.
|
|
PlaneObject.prototype.updateTrack = function(estimate_time) {
|
|
var here = this.position;
|
|
if (!here)
|
|
return;
|
|
|
|
if (this.track_linesegs.length == 0) {
|
|
// Brand new track
|
|
//console.log(this.icao + " new track");
|
|
var newseg = { track : new google.maps.MVCArray([here,here]),
|
|
line : null,
|
|
head_update : this.last_position_time,
|
|
tail_update : this.last_position_time,
|
|
estimated : false,
|
|
ground : (this.altitude === "ground")
|
|
};
|
|
this.track_linesegs.push(newseg);
|
|
this.history_size += 2;
|
|
return;
|
|
}
|
|
|
|
var lastseg = this.track_linesegs[this.track_linesegs.length - 1];
|
|
var lastpos = lastseg.track.getAt(lastseg.track.getLength() - 1);
|
|
var elapsed = (this.last_position_time - lastseg.head_update);
|
|
|
|
var new_data = (here !== lastpos);
|
|
var est_track = (elapsed > estimate_time);
|
|
var ground_track = (this.altitude === "ground");
|
|
|
|
if (!new_data)
|
|
return false;
|
|
|
|
if (est_track) {
|
|
if (!lastseg.estimated) {
|
|
// >5s gap in data, create a new estimated segment
|
|
//console.log(this.icao + " switching to estimated");
|
|
this.track_linesegs.push({ track : new google.maps.MVCArray([lastpos, here]),
|
|
line : null,
|
|
head_update : this.last_position_time,
|
|
estimated : true });
|
|
this.history_size += 2;
|
|
return true;
|
|
}
|
|
|
|
// Append to ongoing estimated line
|
|
//console.log(this.icao + " extending estimated (" + lastseg.track.getLength() + ")");
|
|
lastseg.track.push(here);
|
|
lastseg.head_update = this.last_position_time;
|
|
this.history_size++;
|
|
return true;
|
|
}
|
|
|
|
if (lastseg.estimated) {
|
|
// We are back to good data.
|
|
//console.log(this.icao + " switching to good track");
|
|
this.track_linesegs.push({ track : new google.maps.MVCArray([lastpos, here]),
|
|
line : null,
|
|
head_update : this.last_position_time,
|
|
tail_update : this.last_position_time,
|
|
estimated : false,
|
|
ground : (this.altitude === "ground") });
|
|
this.history_size += 2;
|
|
return true;
|
|
}
|
|
|
|
if ( (lastseg.ground && this.altitude !== "ground") ||
|
|
(!lastseg.ground && this.altitude === "ground") ) {
|
|
//console.log(this.icao + " ground state changed");
|
|
// Create a new segment as the ground state changed.
|
|
// assume the state changed halfway between the two points
|
|
var midpoint = google.maps.geometry.spherical.interpolate(lastpos,here,0.5);
|
|
lastseg.track.push(midpoint);
|
|
this.track_linesegs.push({ track : new google.maps.MVCArray([midpoint,here,here]),
|
|
line : null,
|
|
head_update : this.last_position_time,
|
|
tail_update : this.last_position_time,
|
|
estimated : false,
|
|
ground : (this.altitude === "ground") });
|
|
this.history_size += 4;
|
|
return true;
|
|
}
|
|
|
|
// Add more data to the existing track.
|
|
// We only retain some historical points, at 5+ second intervals,
|
|
// plus the most recent point
|
|
if (this.last_position_time - lastseg.tail_update >= 5) {
|
|
// enough time has elapsed; retain the last point and add a new one
|
|
//console.log(this.icao + " retain last point");
|
|
lastseg.track.push(here);
|
|
lastseg.tail_update = lastseg.head_update;
|
|
this.history_size ++;
|
|
} else {
|
|
// replace the last point with the current position
|
|
lastseg.track.setAt(lastseg.track.getLength()-1, here);
|
|
}
|
|
lastseg.head_update = this.last_position_time;
|
|
return true;
|
|
};
|
|
|
|
// This is to remove the line from the screen if we deselect the plane
|
|
PlaneObject.prototype.clearLines = function() {
|
|
for (var i = 0; i < this.track_linesegs.length; ++i) {
|
|
var seg = this.track_linesegs[i];
|
|
if (seg.line !== null) {
|
|
seg.line.setMap(null);
|
|
seg.line = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
PlaneObject.prototype.getMarkerIconType = function() {
|
|
var lookup = {
|
|
'A1' : 'light',
|
|
'A2' : 'medium',
|
|
'A3' : 'medium',
|
|
'A5' : 'heavy',
|
|
'A7' : 'rotorcraft'
|
|
|
|
};
|
|
|
|
if (this.category === null || !(this.category in lookup))
|
|
return 'generic'
|
|
else
|
|
return lookup[this.category];
|
|
}
|
|
|
|
PlaneObject.prototype.getMarkerColor = function() {
|
|
// Emergency squawks override everything else
|
|
if (this.squawk in SpecialSquawks)
|
|
return SpecialSquawks[this.squawk].markerColor;
|
|
|
|
var h, s, l;
|
|
|
|
if (this.altitude === null) {
|
|
h = ColorByAlt.unknown.h;
|
|
s = ColorByAlt.unknown.s;
|
|
l = ColorByAlt.unknown.l;
|
|
} else if (this.altitude === "ground") {
|
|
h = ColorByAlt.ground.h;
|
|
s = ColorByAlt.ground.s;
|
|
l = ColorByAlt.ground.l;
|
|
} else {
|
|
s = ColorByAlt.air.s;
|
|
l = ColorByAlt.air.l;
|
|
|
|
// find the pair of points the current altitude lies between,
|
|
// and interpolate the hue between those points
|
|
var hpoints = ColorByAlt.air.h;
|
|
h = hpoints[0].val;
|
|
for (var i = hpoints.length-1; i >= 0; --i) {
|
|
if (this.altitude > hpoints[i].alt) {
|
|
if (i == hpoints.length-1) {
|
|
h = hpoints[i].val;
|
|
} else {
|
|
h = hpoints[i].val + (hpoints[i+1].val - hpoints[i].val) * (this.altitude - hpoints[i].alt) / (hpoints[i+1].alt - hpoints[i].alt)
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have not seen a recent position update, change color
|
|
if (this.seen_pos > 15) {
|
|
h += ColorByAlt.stale.h;
|
|
s += ColorByAlt.stale.s;
|
|
l += ColorByAlt.stale.l;
|
|
}
|
|
|
|
// If this marker is selected, change color
|
|
if (this.selected){
|
|
h += ColorByAlt.selected.h;
|
|
s += ColorByAlt.selected.s;
|
|
l += ColorByAlt.selected.l;
|
|
}
|
|
|
|
if (h < 0) {
|
|
h = (h % 360) + 360;
|
|
} else if (h >= 360) {
|
|
h = h % 360;
|
|
}
|
|
|
|
if (s < 5) s = 5;
|
|
else if (s > 95) s = 95;
|
|
|
|
if (l < 5) l = 5;
|
|
else if (l > 95) l = 95;
|
|
|
|
return 'hsl(' + h.toFixed(0) + ',' + s.toFixed(0) + '%,' + l.toFixed(0) + '%)'
|
|
}
|
|
|
|
PlaneObject.prototype.updateIcon = function() {
|
|
var col = this.getMarkerColor();
|
|
var type = this.getMarkerIconType();
|
|
var weight = this.selected ? 2 : 1;
|
|
var rotation = (this.track === null ? 0 : this.track);
|
|
|
|
if (col === this.icon.fillColor && weight === this.icon.strokeWeight && rotation === this.icon.rotation && type == this.icon.type)
|
|
return false; // no changes
|
|
|
|
this.icon.fillColor = col;
|
|
this.icon.strokeWeight = weight;
|
|
this.icon.rotation = rotation;
|
|
this.icon.type = type;
|
|
this.icon.path = MarkerIcons[type].path;
|
|
this.icon.anchor = MarkerIcons[type].anchor;
|
|
this.icon.scale = MarkerIcons[type].scale;
|
|
if (this.marker)
|
|
this.marker.setIcon(this.icon);
|
|
|
|
return true;
|
|
};
|
|
|
|
// Update our data
|
|
PlaneObject.prototype.updateData = function(receiver_timestamp, data) {
|
|
// Update all of our data
|
|
this.messages = data.messages;
|
|
this.rssi = data.rssi;
|
|
this.last_message_time = receiver_timestamp - data.seen;
|
|
|
|
if (typeof data.altitude !== "undefined")
|
|
this.altitude = data.altitude;
|
|
if (typeof data.vert_rate !== "undefined")
|
|
this.vert_rate = data.vert_rate;
|
|
if (typeof data.speed !== "undefined")
|
|
this.speed = data.speed;
|
|
if (typeof data.track !== "undefined")
|
|
this.track = data.track;
|
|
if (typeof data.lat !== "undefined") {
|
|
this.position = new google.maps.LatLng(data.lat, data.lon);
|
|
this.last_position_time = receiver_timestamp - data.seen_pos;
|
|
|
|
if (SitePosition !== null) {
|
|
this.sitedist = google.maps.geometry.spherical.computeDistanceBetween (SitePosition, this.position);
|
|
}
|
|
|
|
this.position_from_mlat = false;
|
|
if (typeof data.mlat !== "undefined") {
|
|
for (var i = 0; i < data.mlat.length; ++i) {
|
|
if (data.mlat[i] === "lat" || data.mlat[i] == "lon") {
|
|
this.position_from_mlat = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (typeof data.flight !== "undefined")
|
|
this.flight = data.flight;
|
|
if (typeof data.squawk !== "undefined")
|
|
this.squawk = data.squawk;
|
|
if (typeof data.category !== "undefined")
|
|
this.category = data.category;
|
|
};
|
|
|
|
PlaneObject.prototype.updateTick = function(receiver_timestamp, last_timestamp) {
|
|
// recompute seen and seen_pos
|
|
this.seen = receiver_timestamp - this.last_message_time;
|
|
this.seen_pos = (this.last_position_time === null ? null : receiver_timestamp - this.last_position_time);
|
|
|
|
// If no packet in over 58 seconds, clear the plane.
|
|
if (this.seen > 58) {
|
|
if (this.visible) {
|
|
//console.log("hiding " + this.icao);
|
|
this.clearMarker();
|
|
this.visible = false;
|
|
if (SelectedPlane == this.icao)
|
|
selectPlaneByHex(null,false);
|
|
}
|
|
} else {
|
|
this.visible = true;
|
|
if (this.position !== null) {
|
|
if (this.updateTrack(receiver_timestamp - last_timestamp + (this.position_from_mlat ? 30 : 5))) {
|
|
this.updateLines();
|
|
this.updateMarker(true);
|
|
} else {
|
|
this.updateMarker(false); // didn't move
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
PlaneObject.prototype.clearMarker = function() {
|
|
if (this.marker) {
|
|
this.marker.setMap(null);
|
|
google.maps.event.clearListeners(this.marker, 'click');
|
|
this.marker = null;
|
|
}
|
|
};
|
|
|
|
// Update our marker on the map
|
|
PlaneObject.prototype.updateMarker = function(moved) {
|
|
if (!this.visible) {
|
|
this.clearMarker();
|
|
return;
|
|
}
|
|
|
|
if (this.marker) {
|
|
if (moved)
|
|
this.marker.setPosition(this.position);
|
|
this.updateIcon();
|
|
} else {
|
|
this.updateIcon();
|
|
this.marker = new google.maps.Marker({
|
|
position: this.position,
|
|
map: GoogleMap,
|
|
icon: this.icon,
|
|
visible: true
|
|
});
|
|
|
|
// Trap clicks for this marker.
|
|
google.maps.event.addListener(this.marker, 'click', selectPlaneByHex.bind(undefined,this.icao,false));
|
|
google.maps.event.addListener(this.marker, 'dblclick', selectPlaneByHex.bind(undefined,this.icao,true));
|
|
}
|
|
|
|
// Setting the marker title
|
|
var title = (this.flight === null || this.flight.length == 0) ? this.icao : (this.flight+' ('+this.icao+')');
|
|
if (title !== this.marker.title)
|
|
this.marker.setTitle(title);
|
|
};
|
|
|
|
// Update our planes tail line,
|
|
PlaneObject.prototype.updateLines = function() {
|
|
if (!this.selected)
|
|
return;
|
|
|
|
for (var i = 0; i < this.track_linesegs.length; ++i) {
|
|
var seg = this.track_linesegs[i];
|
|
if (seg.line === null) {
|
|
// console.log("create line for seg " + i + " with " + seg.track.getLength() + " points" + (seg.estimated ? " (estimated)" : ""));
|
|
// for (var j = 0; j < seg.track.getLength(); j++) {
|
|
// console.log(" point " + j + " at " + seg.track.getAt(j).lat() + "," + seg.track.getAt(j).lng());
|
|
// }
|
|
|
|
if (seg.estimated) {
|
|
var lineSymbol = {
|
|
path: 'M 0,-1 0,1',
|
|
strokeOpacity : 1,
|
|
strokeColor : '#804040',
|
|
strokeWeight : 2,
|
|
scale: 2
|
|
};
|
|
|
|
seg.line = new google.maps.Polyline({
|
|
path: seg.track,
|
|
strokeOpacity: 0,
|
|
icons: [{
|
|
icon: lineSymbol,
|
|
offset: '0',
|
|
repeat: '10px' }],
|
|
map : GoogleMap });
|
|
} else {
|
|
seg.line = new google.maps.Polyline({
|
|
path: seg.track,
|
|
strokeOpacity: 1.0,
|
|
strokeColor: (seg.ground ? '#408040' : '#000000'),
|
|
strokeWeight: 3,
|
|
map: GoogleMap });
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
PlaneObject.prototype.destroy = function() {
|
|
this.clearLines();
|
|
this.clearMarker();
|
|
};
|