8008 lines
250 KiB
JavaScript
8008 lines
250 KiB
JavaScript
// Some global variables are defined in early.js
|
|
// early.js takes care of getting some history files while the html page and
|
|
// some javascript libraries are still loading, hopefully speeding up loading
|
|
|
|
"use strict";
|
|
|
|
// Define our global variables
|
|
let tabHidden = false;
|
|
let webgl = false;
|
|
let webglFeatures = new ol.source.Vector();
|
|
let webglLayer;
|
|
let OLMap = null;
|
|
let OLProj = null;
|
|
let OLProjExtent = null;
|
|
let PlaneIconFeatures = new ol.source.Vector();
|
|
let trailGroup = new ol.Collection();
|
|
let siteCircleLayer;
|
|
let siteCircleFeatures = new ol.source.Vector();
|
|
let locationDotLayer;
|
|
let locationDotFeatures = new ol.source.Vector();
|
|
let iconLayer;
|
|
let trailLayers;
|
|
let heatFeatures = [];
|
|
let heatFeaturesSpread = 1024;
|
|
let heatLayers = [];
|
|
let realHeatFeatures = new ol.source.Vector();
|
|
let realHeat;
|
|
let iconCache = {};
|
|
let addToIconCache = [];
|
|
let lineStyleCache = {};
|
|
let Planes = {};
|
|
let PlanesOrdered = [];
|
|
let PlaneFilter = {};
|
|
let SelectedPlane = null;
|
|
let sp = null;
|
|
let SelPlanes = [];
|
|
let SelectedAllPlanes = false;
|
|
let HighlightedPlane = null;
|
|
let FollowSelected = false;
|
|
let followPos = [];
|
|
let loadFinished = false;
|
|
let loadStart = new Date().getTime();
|
|
let mapResizeTimeout;
|
|
let pointerMoveTimeout;
|
|
let iconSize = 1;
|
|
let debugTracks = false;
|
|
let debugAll = false;
|
|
let trackLabels = false;
|
|
let grouptype_checkbox;
|
|
let multiSelect = false;
|
|
let uat_data = null;
|
|
let enableLabels = false;
|
|
let extendedLabels = 0;
|
|
let mapIsVisible = true;
|
|
let onlyMilitary = false;
|
|
let onlySelected = false;
|
|
let onlyDataSource = null;
|
|
let debug = false;
|
|
let debugJump = false;
|
|
let jumpTo = null;
|
|
let noMLAT = false;
|
|
let noVanish = false;
|
|
let filterTracks = false; // altitude filter: don't filter planes but rather their tracks by altitude
|
|
let refreshId = 0;
|
|
let lastFetch = 0;
|
|
let globeIndexNow = {};
|
|
let globeIndexDist = {};
|
|
let globeIndexSpecialLookup = {};
|
|
let globeTilesViewCount = 0;
|
|
let globeTableLimitBase = 80;
|
|
let globeTableLimit = 80;
|
|
let fetchCounter = 0;
|
|
let lastGlobeExtent;
|
|
let pendingFetches = 0;
|
|
let firstFetch = true;
|
|
let debugCounter = 0;
|
|
let pathName = window.location.pathname.replace(/\/+/, '/') || "/";
|
|
let icaoFilter = null;
|
|
let sourcesFilter = null;
|
|
let sources = ['adsb', ['uat', 'adsr'], 'mlat', 'tisb', 'modeS', 'other', 'adsc'];
|
|
let flagFilter = null;
|
|
let flagFilterValues = ['military', 'pia', 'ladd'];
|
|
let showTrace = false;
|
|
let showTraceExit = false;
|
|
let showTraceWasIsolation = false;
|
|
let showTraceTimestamp = null;
|
|
let traceDate = null;
|
|
let traceDateString = null;
|
|
let traceOpts = {};
|
|
let icaoParam = null;
|
|
let globalScale = 1;
|
|
let userScale = 1;
|
|
let iconScale = 1;
|
|
let labelScale = 1;
|
|
let newWidth = lineWidth;
|
|
let SiteOverride = false;
|
|
let onJumpInput = null;
|
|
let labelFill = null;
|
|
let blackFill = null;
|
|
let labelStroke = null;
|
|
let labelStrokeNarrow = null;
|
|
let bgFill = null;
|
|
let legSel = -1;
|
|
let geoMag = null;
|
|
let solidT = false;
|
|
let lastActive = new Date().getTime();
|
|
let enableOverlays = [];
|
|
let halloween = false;
|
|
let noRegOnly = false;
|
|
let triggerRefresh = 0;
|
|
let firstDraw = true;
|
|
let darkerColors = false;
|
|
let updateLocation = false;
|
|
let autoselect = false;
|
|
let nogpsOnly = false;
|
|
let trace_hist_only = false;
|
|
let traces_high_res = false;
|
|
let show_rId = true;
|
|
let labels_top = false;
|
|
let lockDotCentered = false;
|
|
let overrideMapType = null;
|
|
let layerMoreContrast = false;
|
|
let layerExtraDim = 0;
|
|
let layerExtraContrast = 0;
|
|
let shareFiltersParam = false;
|
|
let lastRequestSize = 0;
|
|
let lastRequestBox = '';
|
|
let nextQuerySelected = 0;
|
|
|
|
let limitUpdates = -1;
|
|
|
|
let infoBlockWidth = baseInfoBlockWidth;
|
|
|
|
const renderBuffer = 45;
|
|
|
|
let shareLink = '';
|
|
|
|
let onMobile = false;
|
|
|
|
let CenterLat = 0;
|
|
let CenterLon = 0;
|
|
let zoomLvl = 5;
|
|
let zoomLvlCache;
|
|
|
|
let TrackedAircraft = 0;
|
|
let globeTrackedAircraft = 0;
|
|
let TrackedAircraftPositions = 0;
|
|
let TrackedHistorySize = 0;
|
|
|
|
let SitePosition = null;
|
|
|
|
// timestamps
|
|
let now = 0;
|
|
let last = 0;
|
|
let uat_now = 0;
|
|
let uat_last = 0;
|
|
let FetchPending = [];
|
|
let FetchPendingUAT = null;
|
|
|
|
let MessageCountHistory = [];
|
|
let MessageRate = 0;
|
|
|
|
let layers;
|
|
let layers_group;
|
|
|
|
const nullStyle = new ol.style.Style({});
|
|
|
|
let estimateStyle;
|
|
let estimateStyleSlim;
|
|
let badLine;
|
|
let badLineMlat;
|
|
let badDot;
|
|
let badDotMlat;
|
|
|
|
let showingReplayBar = false;
|
|
|
|
// TAR1090 application object
|
|
let TAR;
|
|
TAR = (function (global, jQuery, TAR) {
|
|
return TAR;
|
|
}(window, jQuery, TAR || {}));
|
|
|
|
|
|
function processAircraft(ac, init, uat) {
|
|
let isArray = Array.isArray(ac);
|
|
let hex = isArray ? ac[0] : ac.hex;
|
|
|
|
// Do we already have this plane object in Planes?
|
|
// If not make it.
|
|
|
|
/*
|
|
if ( ac.messages < 2) {
|
|
return;
|
|
}
|
|
*/
|
|
if (icaoFilter && !icaoFilter.includes(hex))
|
|
return;
|
|
|
|
if (uatNoTISB && uat && ac.type && ac.type.substring(0,4) == "tisb") {
|
|
// drop non ADS-B planes from UAT (TIS-B)
|
|
return;
|
|
}
|
|
|
|
let plane = Planes[hex]
|
|
|
|
if (!plane) {
|
|
plane = new PlaneObject(hex);
|
|
if (uat)
|
|
plane.uat = true;
|
|
}
|
|
|
|
|
|
if (showTrace)
|
|
return;
|
|
|
|
// Call the function update
|
|
if (globeIndex) {
|
|
if (!onlyMilitary || plane.military || (ac.dbFlags && ac.dbFlags & 1)) {
|
|
plane.updateData(now, last, ac, init);
|
|
} else {
|
|
plane.last_message_time = now - ac.seen;
|
|
}
|
|
return;
|
|
}
|
|
// don't use data if the position is more than 1 second older than the position we have
|
|
if (ac.seen_pos != null && plane.position_time > now - ac.seen_pos + 1)
|
|
return;
|
|
if (!uat) {
|
|
if (!plane.uat
|
|
|| (ac.seen_pos < 2 && plane.seen_pos > 4)
|
|
|| (plane.seen > 10 && ac.seen < 0.8 * plane.seen) || isNaN(plane.seen)
|
|
|| init) {
|
|
plane.uat = false;
|
|
plane.updateData(now, last, ac, init);
|
|
}
|
|
} else {
|
|
if (plane.uat
|
|
|| (ac.seen_pos < 2 && (plane.seen_pos > 4 || plane.dataSource == "mlat"))
|
|
|| (plane.seen > 10 && ac.seen < 0.8 * plane.seen) || isNaN(plane.seen)
|
|
|| init) {
|
|
let tisb = Array.isArray(ac) ? (ac[7] == "tisb") : (ac.tisb != null && ac.tisb.indexOf("lat") >= 0);
|
|
if (tisb && plane.dataSource == "adsb") {
|
|
// ignore TIS-B data for current ADS-B 1090 planes
|
|
} else {
|
|
plane.uat = true;
|
|
plane.updateData(uat_now, uat_last, ac, init);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let backwardsCounter = 0;
|
|
function processReceiverUpdate(data, init) {
|
|
// update now and last
|
|
let uat = data.uat_978;
|
|
if (uat) {
|
|
if (data.now <= uat_now)
|
|
return;
|
|
uat_last = uat_now;
|
|
uat_now = data.now;
|
|
} else {
|
|
if (data.now <= now && !globeIndex) {
|
|
if (data.now < now) {
|
|
console.log('timestep backwards, ignoring data:' + now + ' -> ' + data.now);
|
|
if (backwardsCounter++ > 5) {
|
|
backwardsCounter = 0;
|
|
console.log('resetting now:' + now + ' -> ' + data.now);
|
|
now = data.now;
|
|
last = now - 1;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (data.now > now) {
|
|
backwardsCounter = 0;
|
|
last = now;
|
|
now = data.now;
|
|
}
|
|
}
|
|
|
|
if (globeIndex) {
|
|
if ((showGrid || loStore['globeGrid'] == 'true')
|
|
&& globeIndexNow[data.globeIndex] == null)
|
|
drawTileBorder(data);
|
|
globeTrackedAircraft = data.global_ac_count_withpos;
|
|
globeIndexNow[data.globeIndex] = data.now;
|
|
}
|
|
|
|
if (!uat && !init && !globeIndex)
|
|
updateMessageRate(data);
|
|
|
|
// Loop through all the planes in the data packet
|
|
for (let j=0; j < data.aircraft.length; j++)
|
|
processAircraft(data.aircraft[j], init, uat);
|
|
}
|
|
|
|
let debugFetch = false;
|
|
let C429 = 0;
|
|
let fetchCalls = 0;
|
|
function fetchData(options) {
|
|
options = options || {};
|
|
if (heatmap || replay || showTrace || pTracks || !loadFinished || inhibitFetch) {
|
|
return;
|
|
}
|
|
let currentTime = new Date().getTime();
|
|
|
|
if (!options.force) {
|
|
if (
|
|
currentTime - lastFetch < refreshInt()
|
|
|| pendingFetches > 0
|
|
|| OLMap.getView().getInteracting()
|
|
|| OLMap.getView().getAnimating()
|
|
) {
|
|
return;
|
|
}
|
|
}
|
|
if (debugFetch)
|
|
console.log((currentTime - lastFetch)/1000);
|
|
lastFetch = currentTime;
|
|
|
|
FetchPending = [];
|
|
if (FetchPendingUAT != null) {
|
|
// don't double up on fetches, let the last one resolve
|
|
return;
|
|
}
|
|
|
|
//console.timeEnd("Starting Fetch");
|
|
//console.time("Starting Fetch");
|
|
|
|
if (limitUpdates != -1 && fetchCalls > limitUpdates) {
|
|
return;
|
|
}
|
|
fetchCalls++;
|
|
|
|
if (enable_uat) {
|
|
FetchPendingUAT = jQuery.ajax({ url: 'chunks/978.json',
|
|
dataType: 'json' });
|
|
|
|
FetchPendingUAT.done(function(data) {
|
|
uat_data = data;
|
|
FetchPendingUAT = null;
|
|
});
|
|
FetchPendingUAT.fail(function(jqxhr, status, error) {
|
|
FetchPendingUAT = null;
|
|
});
|
|
}
|
|
|
|
let ac_url = [];
|
|
if (uuid != null) {
|
|
for (let i in uuid) {
|
|
ac_url.push('uuid/?feed=' + uuid[i]);
|
|
}
|
|
} else if (reApi) {
|
|
let url = 're-api/?' + (binCraft ? 'binCraft' : 'json');
|
|
url += zstd ? '&zstd' : '';
|
|
url += onlyMilitary ? '&filter_mil' : '';
|
|
lastRequestBox = requestBoxString();
|
|
if (icaoFilter) {
|
|
if (icaoFilter.length > 0) {
|
|
url += '&find_hex='
|
|
for (let k in icaoFilter) {
|
|
url += icaoFilter[k] + ','
|
|
}
|
|
url = url.slice(0, -1); // remove trailing comma
|
|
}
|
|
} else if (onlySelected && SelPlanes.length > 0) {
|
|
url += '&find_hex='
|
|
for (let k in SelPlanes) {
|
|
url += SelPlanes[k].icao + ','
|
|
}
|
|
url = url.slice(0, -1); // remove trailing comma
|
|
} else {
|
|
url += '&box=' + lastRequestBox;
|
|
|
|
if (SelPlanes.length > 0) {
|
|
url += '&find_hex='
|
|
for (let k in SelPlanes) {
|
|
url += SelPlanes[k].icao + ','
|
|
}
|
|
url = url.slice(0, -1); // remove trailing comma
|
|
}
|
|
}
|
|
|
|
ac_url.push(url);
|
|
//console.log(url);
|
|
|
|
} else if (globeIndex) {
|
|
let indexes = globeIndexes();
|
|
const ancient = (currentTime - 2 * refreshInt() / globeSimLoad * globeTilesViewCount) / 1000;
|
|
for (let i in indexes) {
|
|
const k = indexes[i];
|
|
if (globeIndexNow[k] < ancient) {
|
|
globeIndexNow[k] = null;
|
|
}
|
|
}
|
|
indexes.sort(function(x,y) {
|
|
if (!globeIndexNow[x] && !globeIndexNow[y]) {
|
|
return globeIndexDist[x] - globeIndexDist[y];
|
|
}
|
|
if (globeIndexNow[x] == null)
|
|
return -1;
|
|
if (globeIndexNow[y] == null)
|
|
return 1;
|
|
return (globeIndexNow[x] - globeIndexNow[y]);
|
|
});
|
|
|
|
if (binCraft && onlyMilitary && zoomLvl < 5.5) {
|
|
if (zstd) {
|
|
ac_url.push('data/globeMil_42777.binCraft.zst');
|
|
} else {
|
|
ac_url.push('data/globeMil_42777.binCraft');
|
|
}
|
|
} else {
|
|
|
|
indexes = indexes.slice(0, globeSimLoad);
|
|
|
|
let suffix = zstd ? '.binCraft.zst' : (binCraft ? '.binCraft' : '.json');
|
|
let mid = (binCraft && onlyMilitary) ? 'Mil_' : '_';
|
|
for (let i in indexes) {
|
|
ac_url.push('data/globe' + mid + indexes[i].toString().padStart(4, '0') + suffix);
|
|
}
|
|
}
|
|
} else if (zstd) {
|
|
ac_url.push('data/aircraft.binCraft.zst');
|
|
} else if (binCraft) {
|
|
ac_url.push('data/aircraft.binCraft');
|
|
} else {
|
|
ac_url.push('data/aircraft.json');
|
|
}
|
|
|
|
pendingFetches += ac_url.length;
|
|
fetchCounter += ac_url.length;
|
|
|
|
for (let i in ac_url) {
|
|
//console.log(ac_url[i]);
|
|
let req;
|
|
if (binCraft || zstd) {
|
|
req = jQuery.ajax({
|
|
url: `${ac_url[i]}`, method: 'GET',
|
|
xhr: arraybufferRequest,
|
|
timeout: 15000,
|
|
urlIndex: i,
|
|
});
|
|
} else {
|
|
req = jQuery.ajax({ url: `${ac_url[i]}`, dataType: 'json', urlIndex: i });
|
|
}
|
|
FetchPending.push(req);
|
|
|
|
req
|
|
.done(function(data) {
|
|
pendingFetches--;
|
|
if (data == null) {
|
|
return;
|
|
}
|
|
if (zstd) {
|
|
let arr = new Uint8Array(data);
|
|
lastRequestSize = arr.byteLength;
|
|
let res;
|
|
try {
|
|
res = zstdDecode( arr, 0 );
|
|
} catch (e) {
|
|
let errText = e.message;
|
|
console.log(errText);
|
|
jQuery("#update_error_detail").text(errText);
|
|
jQuery("#update_error").css('display','block');
|
|
return;
|
|
}
|
|
let arrayBuffer = res.buffer
|
|
// return type is Uint8Array, wqi requires the ArrayBuffer
|
|
data = { buffer: arrayBuffer, };
|
|
wqi(data);
|
|
} else if (binCraft) {
|
|
lastRequestSize = data.byteLength / 2;
|
|
data = { buffer: data, };
|
|
wqi(data);
|
|
}
|
|
data.urlIndex = this.urlIndex;
|
|
|
|
if (!data.aircraft || !data.now) {
|
|
let error = data.error;
|
|
if (error) {
|
|
jQuery("#update_error_detail").text(error);
|
|
jQuery("#update_error").css('display','block');
|
|
StaleReceiverCount++;
|
|
}
|
|
return;
|
|
}
|
|
|
|
//console.time("Process " + data.globeIndex);
|
|
processReceiverUpdate(data);
|
|
//console.timeEnd("Process " + data.globeIndex);
|
|
data = null;
|
|
|
|
if (uat_data) {
|
|
processReceiverUpdate(uat_data);
|
|
uat_data = null;
|
|
}
|
|
|
|
if (pendingFetches <= 0 && !tabHidden) {
|
|
triggerRefresh++;
|
|
checkMovement();
|
|
if (firstFetch) {
|
|
firstFetch = false;
|
|
if (uuid) {
|
|
const ext = myExtent(OLMap.getView().calculateExtent(OLMap.getSize()));
|
|
let jump = true;
|
|
for (let i = 0; i < PlanesOrdered.length; ++i) {
|
|
const plane = PlanesOrdered[i];
|
|
if (plane.visible && inView(plane.position, ext)) {
|
|
jump = false;
|
|
break;
|
|
}
|
|
}
|
|
if (jump) {
|
|
followRandomPlane();
|
|
deselectAllPlanes();
|
|
OLMap.getView().setZoom(6);
|
|
}
|
|
}
|
|
checkRefresh();
|
|
}
|
|
}
|
|
|
|
|
|
// Check for stale receiver data
|
|
if (last == now && !globeIndex) {
|
|
StaleReceiverCount++;
|
|
if (StaleReceiverCount > 5) {
|
|
jQuery("#update_error_detail").text("The data from the server hasn't been updated in a while.");
|
|
jQuery("#update_error").css('display','block');
|
|
}
|
|
} else if (StaleReceiverCount > 0){
|
|
StaleReceiverCount = 0;
|
|
jQuery("#update_error").css('display','none');
|
|
}
|
|
})
|
|
.fail(function(jqxhr, status, error) {
|
|
pendingFetches--;
|
|
if (pendingFetches <= 0 && !tabHidden) {
|
|
triggerRefresh++;
|
|
checkMovement();
|
|
}
|
|
status = jqxhr.status;
|
|
if (jqxhr.readyState == 0) error = "Can't connect to server, check your network!";
|
|
let errText = status + (error ? (": " + error) : "");
|
|
console.log(jqxhr);
|
|
console.log(error);
|
|
if (status != 429 && status != '429') {
|
|
jQuery("#update_error_detail").text(errText);
|
|
jQuery("#update_error").css('display','block');
|
|
StaleReceiverCount++;
|
|
} else {
|
|
if (C429++ > 16) {
|
|
globeRateUpdate();
|
|
C429 = 0;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// this function is called from index.html on body load
|
|
// kicks off the whole rabbit hole
|
|
function initialize() {
|
|
if (usp.has('iconTest')) {
|
|
iconTest();
|
|
return;
|
|
}
|
|
|
|
jQuery.when(configureReceiver, heatmapDefer).done(function() {
|
|
|
|
if (receiverJson) {
|
|
if (receiverJson.trace_hist_only)
|
|
trace_hist_only = true;
|
|
if (receiverJson.json_trace_interval < 2)
|
|
traces_high_res = true;
|
|
if (receiverJson.lat != null) {
|
|
SiteLat = receiverJson.lat;
|
|
SiteLon = receiverJson.lon;
|
|
DefaultCenterLat = receiverJson.lat;
|
|
DefaultCenterLon = receiverJson.lon;
|
|
}
|
|
if (receiverJson.jaeroTimeout) {
|
|
jaeroTimeout = receiverJson.jaeroTimeout * 60;
|
|
}
|
|
|
|
|
|
if (receiverJson.readsb) {
|
|
positionFilter = false;
|
|
altitudeFilter = false;
|
|
}
|
|
}
|
|
configureReceiver = null;
|
|
|
|
// Initialize stuff
|
|
initPage();
|
|
initMap();
|
|
|
|
processQueryToggles();
|
|
|
|
// Wait for history item downloads and append them to the buffer
|
|
push_history();
|
|
|
|
jQuery.when(historyLoaded).done(function() {
|
|
if (!zstdDecode) {
|
|
startPage();
|
|
} else {
|
|
try {
|
|
zstddec.promise.then(function() {
|
|
startPage();
|
|
});
|
|
} catch (e) {
|
|
webAssemblyFail(e);
|
|
startPage();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
function processQueryToggles() {
|
|
if (!usp.has('toggles')) {
|
|
return;
|
|
}
|
|
let todo;
|
|
try {
|
|
todo = usp.get('toggles').split(',');;
|
|
} catch (e) {
|
|
console.error(e);
|
|
return;
|
|
}
|
|
while (todo.length >= 2) {
|
|
let key = todo.shift();
|
|
let value = todo.shift();
|
|
let state = true;
|
|
if (value == 'false' || value == '0') {
|
|
state = false;
|
|
}
|
|
try {
|
|
toggles[key].toggle(state, "init");
|
|
console.log((state ? "Enabled" : "Disabled") + " setting: " + key);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function replaySpeedChange(arg) {
|
|
traceOpts.replaySpeed = arg;
|
|
console.log(arg);
|
|
if (traceOpts.animate)
|
|
return;
|
|
legShift(0);
|
|
};
|
|
|
|
|
|
function initPage() {
|
|
let value;
|
|
onMobile = window.mobilecheck();
|
|
|
|
if (uk_advisory) {
|
|
|
|
labels_top = true;
|
|
tempTrails = true;
|
|
tempTrailsTimeout = 45;
|
|
SiteCirclesDistances = new Array(5, 10, 20);
|
|
SiteCirclesLineDash = [5, 5];
|
|
SiteCirclesColors = ['#2b3436', '#2b3436', '#2b3436'];
|
|
defaultOverlays.push('uka_airports');
|
|
defaultOverlays.push('uka_airspaces');
|
|
defaultOverlays.push('uka_runways');
|
|
defaultOverlays.push('uka_shoreham');
|
|
MapType_tar1090 = 'carto_light_all';
|
|
lineWidth=4;
|
|
enableLabels=true;
|
|
}
|
|
|
|
if (usp.has('limitUpdates')) {
|
|
let tmp = parseInt(usp.get('limitUpdates'));
|
|
if (!isNaN(tmp))
|
|
limitUpdates = tmp;
|
|
}
|
|
|
|
if (usp.has('nowebgl')) {
|
|
loStore['webgl'] = "false";
|
|
}
|
|
|
|
if (usp.has('showGrid')) {
|
|
showGrid = true;
|
|
loStore['layer_site_pos'] = 'true';
|
|
}
|
|
|
|
if (usp.has('halloween'))
|
|
halloween = true;
|
|
|
|
if (usp.has('onlyDataSource'))
|
|
onlyDataSource = usp.get('onlyDataSource');
|
|
|
|
if (usp.has('outlineWidth')) {
|
|
let tmp = parseInt(usp.get('outlineWidth'));
|
|
if (!isNaN(tmp))
|
|
outlineWidth = tmp;
|
|
}
|
|
|
|
if (usp.has('kiosk')) {
|
|
tempTrails = true;
|
|
hideButtons = true;
|
|
userScale = 2;
|
|
}
|
|
|
|
if (pTracks) {
|
|
noVanish = true;
|
|
buttonActive('#P', noVanish);
|
|
filterTracks = true;
|
|
selectAllPlanes();
|
|
}
|
|
|
|
if (usp.has('mobile'))
|
|
onMobile = true;
|
|
if (usp.has('desktop'))
|
|
onMobile = false;
|
|
|
|
if (usp.has('hideSidebar'))
|
|
loStore['sidebar_visible'] = "false";
|
|
if (usp.has('sidebarWidth')) {
|
|
loStore['sidebar_width'] = usp.get('sidebarWidth');
|
|
loStore['sidebar_visible'] = "true";
|
|
}
|
|
|
|
if (usp.has('SiteLat') && usp.has('SiteLon')) {
|
|
let lat = parseFloat(usp.get('SiteLat'));
|
|
let lon = parseFloat(usp.get('SiteLon'));
|
|
if (!isNaN(lat) && !isNaN(lon)) {
|
|
if (usp.has('SiteNosave')) {
|
|
SiteLat = CenterLat = DefaultCenterLat = lat;
|
|
SiteLon = CenterLon = DefaultCenterLon = lon;
|
|
SiteOverride = true;
|
|
} else {
|
|
loStore['SiteLat'] = lat;
|
|
loStore['SiteLon'] = lon;
|
|
}
|
|
}
|
|
}
|
|
if (loStore['SiteLat'] != null && loStore['SiteLon'] != null) {
|
|
if (usp.has('SiteClear')) {
|
|
loStore.removeItem('SiteLat');
|
|
loStore.removeItem('SiteLon');
|
|
} else if (!usp.has('SiteNosave')) {
|
|
SiteLat = CenterLat = DefaultCenterLat = parseFloat(loStore['SiteLat']);
|
|
SiteLon = CenterLon = DefaultCenterLon = parseFloat(loStore['SiteLon']);
|
|
SiteOverride = true;
|
|
}
|
|
}
|
|
|
|
if (usp.has('allTracks')) {
|
|
SelectedAllPlanes = true;
|
|
buttonActive('#T', SelectedAllPlanes);
|
|
}
|
|
if (usp.has('tempTrails')) {
|
|
tempTrails = true;
|
|
let tmp = parseInt(usp.get('tempTrails'));
|
|
if (tmp > 0)
|
|
tempTrailsTimeout = tmp;
|
|
}
|
|
if (usp.has('squareMania')) {
|
|
squareMania = true;
|
|
}
|
|
|
|
if (usp.has('darkerColors')) {
|
|
darkerColors = true;
|
|
}
|
|
|
|
if (usp.has('mapDim')) {
|
|
let dim = parseFloat(usp.get('mapDim'));
|
|
if (!isNaN(dim))
|
|
mapDimPercentage = dim;
|
|
} else if (heatmap) {
|
|
mapDimPercentage = 0.6;
|
|
MapDim = true;
|
|
}
|
|
|
|
if (usp.has('noRegOnly'))
|
|
noRegOnly = true;
|
|
|
|
if (usp.has('nogpsOnly') || usp.has('badgps'))
|
|
nogpsOnly = true;
|
|
|
|
if (usp.has('mapContrast')) {
|
|
let contrast = parseFloat(usp.get('mapContrast'));
|
|
if (!isNaN(contrast))
|
|
mapContrastPercentage = contrast;
|
|
}
|
|
|
|
if (value = usp.getFloat('labelScale')) {
|
|
labelScale = value;
|
|
}
|
|
|
|
if (value = usp.getFloat('largeMode')) {
|
|
userScale = Math.pow(1.2, value) / 1.2;
|
|
iconScale = 1;
|
|
}
|
|
|
|
if (value = usp.getFloat('iconScale')) {
|
|
iconScale = value;
|
|
} else if (loStore['iconScale'] != null) {
|
|
iconScale = loStore['iconScale'];
|
|
}
|
|
|
|
if (value = usp.getFloat('scale')) {
|
|
userScale = value;
|
|
} else if (loStore['userScale'] != null) {
|
|
userScale = loStore['userScale'];
|
|
}
|
|
|
|
const slideBase = 0.6;
|
|
jQuery('#iconScaleSlider').slider({
|
|
value: Math.pow(iconScale, 1 / slideBase),
|
|
step: 0.02,
|
|
min: 0.1,
|
|
max: 3,
|
|
change: function(event, ui) {
|
|
iconScale = Math.pow(ui.value, slideBase);
|
|
checkScale();
|
|
mapRefresh();
|
|
loStore['iconScale'] = iconScale;
|
|
},
|
|
});
|
|
|
|
jQuery('#userScaleSlider').slider({
|
|
value: Math.pow(userScale, 1 / slideBase),
|
|
step: 0.02,
|
|
min: 0.5,
|
|
max: 3,
|
|
change: function(event, ui) {
|
|
userScale = Math.pow(ui.value, slideBase);
|
|
checkScale();
|
|
mapRefresh();
|
|
loStore['userScale'] = userScale;
|
|
|
|
setGlobalScale(userScale);
|
|
},
|
|
});
|
|
setGlobalScale(userScale, "init");
|
|
|
|
if (usp.has('hideButtons'))
|
|
hideButtons = true;
|
|
|
|
if (usp.has('baseMap')) {
|
|
overrideMapType = usp.get('baseMap');
|
|
}
|
|
|
|
if (usp.has('offlineMap')) {
|
|
overrideMapType = 'osm_tiles_offline';
|
|
}
|
|
|
|
if (usp.has('overlays'))
|
|
enableOverlays = usp.get('overlays').split(',');
|
|
|
|
if (value = usp.get('icaoFilter')) {
|
|
icaoFilter = value.toLowerCase().split(',');
|
|
}
|
|
|
|
if (value = usp.getFloat('filterMaxRange')) {
|
|
filterMaxRange = value;
|
|
}
|
|
filterMaxRange *= 1852; // convert from nmi to meters
|
|
|
|
|
|
if (value = usp.getFloat('mapOrientation')) {
|
|
mapOrientation = value;
|
|
}
|
|
mapOrientation *= (Math.PI/180); // adjust to radians
|
|
|
|
if (usp.has('r') || usp.has('replay')) {
|
|
let numbers = (usp.get('r') || usp.get('replay') || "").split(/(?:-|:)/);
|
|
let ts = new Date();
|
|
if (numbers.length == 5) {
|
|
ts.setUTCFullYear(numbers[0]);
|
|
ts.setUTCMonth(numbers[1] - 1);
|
|
ts.setUTCDate(numbers[2]);
|
|
ts.setUTCHours(numbers[3]);
|
|
ts.setUTCMinutes(numbers[4]);
|
|
}
|
|
if (isNaN(ts)) {
|
|
ts = new Date();
|
|
}
|
|
console.log(ts);
|
|
replay = replayDefaults(ts);
|
|
}
|
|
|
|
//Pulling filters from params
|
|
if (usp.has('filterAltMin')) {
|
|
const minAlt = usp.getInt('filterAltMin');
|
|
if (minAlt !== null) {
|
|
PlaneFilter.minAltitude = minAlt;
|
|
PlaneFilter.enabled = true;
|
|
PlaneFilter.maxAltitude = 1000000;
|
|
}
|
|
}
|
|
if (usp.has('filterAltMax')) {
|
|
const maxAlt = usp.getInt('filterAltMax');
|
|
if (maxAlt !== null) {
|
|
PlaneFilter.maxAltitude = maxAlt;
|
|
PlaneFilter.enabled = true;
|
|
if (PlaneFilter.minAltitude === undefined) {
|
|
PlaneFilter.minAltitude = -1000000;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (usp.has('filterCallSign')) {
|
|
PlaneFilter.callsign = usp.get('filterCallSign');
|
|
shareFiltersParam = true;
|
|
}
|
|
if (usp.has('filterType')) {
|
|
PlaneFilter.type = usp.get('filterType');
|
|
shareFiltersParam = true;
|
|
}
|
|
if (usp.has('filterDescription')) {
|
|
PlaneFilter.description = usp.get('filterDescription');
|
|
shareFiltersParam = true;
|
|
}
|
|
if (usp.has('filterIcao')) {
|
|
PlaneFilter.icao = usp.get('filterIcao');
|
|
shareFiltersParam = true;
|
|
}
|
|
|
|
if (usp.has('filterSources')) {
|
|
PlaneFilter.sources = usp.get('filterSources').split(',');
|
|
shareFiltersParam = true;
|
|
}
|
|
if (usp.has('filterDbFlag')) {
|
|
PlaneFilter.flagFilter = usp.get('filterDbFlag').split(',');
|
|
shareFiltersParam = true;
|
|
}
|
|
|
|
|
|
if (onMobile)
|
|
enableMouseover = false;
|
|
|
|
if (false && iOSVersion() <= 12 && !('PointerEvent' in window)) {
|
|
jQuery("#generic_error_detail").text("Enable Settings - Safari - Advanced - Experimental features - Pointer Events");
|
|
jQuery("#generic_error").css('display','block');
|
|
setTimeout(function() {
|
|
jQuery("#generic_error").css('display','none');
|
|
}, 30000);
|
|
}
|
|
|
|
if (loStore['enableLabels'] == 'true' || usp.has('enableLabels')) {
|
|
toggleLabels();
|
|
}
|
|
if (usp.has('extendedLabels')) {
|
|
extendedLabels = parseInt(usp.getFloat('extendedLabels'));
|
|
toggleExtendedLabels({ noIncrement: true });
|
|
} else if (loStore['extendedLabels']) {
|
|
extendedLabels = parseInt(loStore['extendedLabels']);
|
|
toggleExtendedLabels({ noIncrement: true });
|
|
}
|
|
if (loStore['trackLabels'] == "true" || usp.has('trackLabels')) {
|
|
toggleTrackLabels();
|
|
}
|
|
if (loStore['tableInView'] == "true" || usp.has('tableInView')) {
|
|
toggleTableInView(true);
|
|
}
|
|
if (loStore['debug'] == "true")
|
|
debug = true;
|
|
if (loStore['debugPosFilter'] == "true")
|
|
debugPosFilter = true;
|
|
|
|
if (loStore['noVanish'] == "true" || usp.has('noVanish')) {
|
|
noVanish = true;
|
|
//filterTracks = noVanish;
|
|
//loStore['noVanish'] = "false";
|
|
buttonActive('#P', noVanish);
|
|
}
|
|
|
|
jQuery('#tabs').tabs({
|
|
active: loStore['active_tab'],
|
|
activate: function (event, ui) {
|
|
loStore['active_tab'] = jQuery("#tabs").tabs("option", "active");
|
|
},
|
|
collapsible: true
|
|
});
|
|
|
|
// Set page basics
|
|
document.title = PageName;
|
|
|
|
initializeUnitsSelector();
|
|
TAR.planeMan.init();
|
|
|
|
// Set up map/sidebar splitter
|
|
jQuery("#sidebar_container").resizable({
|
|
handles: {
|
|
w: '#splitter'
|
|
},
|
|
minWidth: 150,
|
|
maxWidth: (jQuery(window).innerWidth() *0.8),
|
|
});
|
|
|
|
jQuery("#splitter").dblclick(function() {
|
|
jQuery('#legend').hide();
|
|
jQuery('#sidebar_container').width('auto');
|
|
updateMapSize();
|
|
loStore['sidebar_width'] = jQuery('#sidebar_container').width();
|
|
jQuery('#sidebar_container').width(loStore['sidebar_width']);
|
|
jQuery('#legend').show();
|
|
});
|
|
|
|
if (loStore['sidebar_width'] != null)
|
|
jQuery('#sidebar_container').width(loStore['sidebar_width']);
|
|
else
|
|
jQuery('#sidebar_container').width('25%');
|
|
|
|
if (jQuery('#sidebar_container').width() > jQuery(window).innerWidth() *0.8)
|
|
jQuery('#sidebar_container').width('30%');
|
|
|
|
loStore['sidebar_width'] = jQuery('#sidebar_container').width();
|
|
|
|
jQuery('#sidebar_container').on('resize', function() {
|
|
loStore['sidebar_width'] = jQuery('#sidebar_container').width();
|
|
});
|
|
|
|
// Set up event handlers for buttons
|
|
jQuery("#expand_sidebar_button").click(expandSidebar);
|
|
jQuery("#shrink_sidebar_button").click(showMap);
|
|
|
|
// Set up altitude filter button event handlers and validation options
|
|
jQuery("#altitude_filter_form").submit(onFilterByAltitude);
|
|
jQuery("#callsign_filter_form").submit(updateCallsignFilter);
|
|
jQuery("#type_filter_form").submit(updateTypeFilter);
|
|
jQuery("#description_filter_form").submit(updateDescriptionFilter);
|
|
jQuery("#icao_filter_form").submit(updateIcaoFilter);
|
|
jQuery("#source_filter_form").submit(updateSourceFilter);
|
|
jQuery("#flag_filter_form").submit(updateFlagFilter);
|
|
|
|
// Initialize other controls
|
|
jQuery("#search_form").submit(onSearch);
|
|
jQuery("#search_clear_button").click(onSearchClear);
|
|
jQuery("#jump_clear_button").click(function() {
|
|
jQuery("#jump_input").val("");
|
|
jQuery("#jump_input").blur();
|
|
});
|
|
jQuery("#jump_form").submit(onJump);
|
|
|
|
jQuery("#show_trace").click(toggleShowTrace);
|
|
jQuery("#trace_back_1d").click(function() {shiftTrace(-1)});
|
|
jQuery("#trace_jump_1d").click(function() {shiftTrace(1)});
|
|
|
|
jQuery("#histDatePicker").datepicker({
|
|
maxDate: '+1d',
|
|
dateFormat: "yy-mm-dd",
|
|
onSelect: function(date){
|
|
setTraceDate({string: date});
|
|
shiftTrace();
|
|
jQuery("#histDatePicker").blur();
|
|
},
|
|
autoSize: true,
|
|
onClose: !onMobile ? null : function(dateText, inst){
|
|
jQuery("#histDatePicker").attr("disabled", false);
|
|
},
|
|
beforeShow: !onMobile ? null : function(input, inst){
|
|
jQuery("#histDatePicker").attr("disabled", true);
|
|
},
|
|
});
|
|
|
|
jQuery("#replayPlay").click(function(){
|
|
|
|
if (replay.playing){
|
|
//if playing, pause.
|
|
playReplay(false);
|
|
|
|
} else {
|
|
//if paused, play.
|
|
playReplay(true);
|
|
}
|
|
});
|
|
|
|
jQuery("#leg_prev").click(function() {legShift(-1)});
|
|
jQuery("#leg_next").click(function() {legShift(1)});
|
|
|
|
jQuery("#altitude_filter_reset_button").click(onResetAltitudeFilter);
|
|
jQuery("#callsign_filter_reset_button").click(onResetCallsignFilter);
|
|
jQuery("#type_filter_reset_button").click(onResetTypeFilter);
|
|
jQuery("#description_filter_reset_button").click(onResetDescriptionFilter);
|
|
jQuery("#icao_filter_reset_button").click(onResetIcaoFilter);
|
|
jQuery("#source_filter_reset_button").click(onResetSourceFilter);
|
|
jQuery("#flag_filter_reset_button").click(onResetFlagFilter);
|
|
|
|
jQuery('#settingsCog').on('click', function() {
|
|
jQuery('#settings_infoblock').toggle();
|
|
});
|
|
|
|
jQuery('#settings_close').on('click', function() {
|
|
jQuery('#settings_infoblock').hide();
|
|
});
|
|
|
|
jQuery('#groundvehicle_filter').on('click', function() {
|
|
filterGroundVehicles(true);
|
|
refresh();
|
|
});
|
|
|
|
jQuery('#blockedmlat_filter').on('click', function() {
|
|
filterBlockedMLAT(true);
|
|
refresh();
|
|
});
|
|
|
|
jQuery('#grouptype_checkbox').on('click', function() {
|
|
if (jQuery('#grouptype_checkbox').hasClass('settingsCheckboxChecked')) {
|
|
TAR.planeMan.cols.distance.sort();
|
|
} else {
|
|
TAR.planeMan.cols.data_source.sort();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "lastLeg",
|
|
display: "Last Leg only",
|
|
container: "#settingsLeft",
|
|
init: true,
|
|
setState: function(state) {
|
|
lastLeg = state;
|
|
if (loadFinished && !showTrace) {
|
|
for (let i in SelPlanes) {
|
|
SelPlanes[i].processTrace();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
new Toggle({
|
|
key: "labelsGeom",
|
|
display: "Labels: geom. alt. (WGS84)",
|
|
container: "#settingsLeft",
|
|
init: labelsGeom,
|
|
setState: function(state) {
|
|
labelsGeom = state;
|
|
if (loadFinished) {
|
|
remakeTrails();
|
|
refreshSelected();
|
|
}
|
|
}
|
|
});
|
|
new Toggle({
|
|
key: "geomUseEGM",
|
|
display: "Geom. alt.: WGS84 -> EGM conversion (long load)",
|
|
container: "#settingsLeft",
|
|
init: geomUseEGM,
|
|
setState: function(state) {
|
|
geomUseEGM = state;
|
|
if (geomUseEGM) {
|
|
jQuery('#selected_altitude_geom1')
|
|
jQuery('#selected_altitude_geom1_title').updateText('EGM96 altitude');
|
|
jQuery('#selected_altitude_geom2_title').updateText('Geom. EGM96');
|
|
let egm = loadEGM();
|
|
if (egm) {
|
|
egm.addEventListener('load', function() {
|
|
remakeTrails();
|
|
refreshSelected();
|
|
});
|
|
return;
|
|
}
|
|
} else {
|
|
jQuery('#selected_altitude_geom1_title').updateText('WGS84 altitude');
|
|
jQuery('#selected_altitude_geom2_title').updateText('Geom. WGS84');
|
|
}
|
|
if (loadFinished) {
|
|
remakeTrails();
|
|
refreshSelected();
|
|
}
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "utcTimesLive",
|
|
display: "Live track labels: UTC",
|
|
container: "#settingsLeft",
|
|
init: utcTimesLive,
|
|
setState: function(state) {
|
|
utcTimesLive = state;
|
|
remakeTrails();
|
|
refreshSelected();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "utcTimesHistoric",
|
|
display: "Historic track labels: UTC",
|
|
container: "#settingsLeft",
|
|
init: utcTimesHistoric,
|
|
setState: function(state) {
|
|
utcTimesHistoric = state;
|
|
remakeTrails();
|
|
refreshSelected();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "windLabelsSlim",
|
|
display: "Smaller wind labels",
|
|
container: "#settingsLeft",
|
|
init: windLabelsSlim,
|
|
setState: function(state) {
|
|
windLabelsSlim = state;
|
|
if (!loadFinished)
|
|
return;
|
|
for (let key in PlanesOrdered) {
|
|
PlanesOrdered[key].updateMarker();
|
|
}
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "showLabelUnits",
|
|
display: "Label units",
|
|
container: "#settingsLeft",
|
|
init: showLabelUnits,
|
|
setState: function(state) {
|
|
showLabelUnits = state;
|
|
if (!loadFinished)
|
|
return;
|
|
for (let key in PlanesOrdered) {
|
|
PlanesOrdered[key].updateMarker();
|
|
}
|
|
remakeTrails();
|
|
refreshSelected();
|
|
}
|
|
});
|
|
|
|
|
|
jQuery('#tStop').on('click', function() { traceOpts.replaySpeed = 0; gotoTime(traceOpts.showTime); });
|
|
jQuery('#t1x').on('click', function() { replaySpeedChange(1); });
|
|
jQuery('#t5x').on('click', function() { replaySpeedChange(5); });
|
|
jQuery('#t10x').on('click', function() { replaySpeedChange(10); });
|
|
jQuery('#t20x').on('click', function() { replaySpeedChange(20); });
|
|
jQuery('#t40x').on('click', function() { replaySpeedChange(40); });
|
|
|
|
new Toggle({
|
|
key: "shareFilters",
|
|
display: "Include Filters In URLs",
|
|
container: "#settingsRight",
|
|
init: false,
|
|
setState: function(state) {
|
|
updateAddressBar();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "debugTracks",
|
|
display: "Debug Tracks",
|
|
container: "#settingsRight",
|
|
init: false,
|
|
setState: function(state) {
|
|
debugTracks = state;
|
|
remakeTrails();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "debugAll",
|
|
display: "Debug show all",
|
|
container: "#settingsRight",
|
|
init: false,
|
|
setState: function(state) {
|
|
if (state)
|
|
debugAll = true;
|
|
else
|
|
debugAll = false;
|
|
}
|
|
});
|
|
|
|
/*
|
|
new Toggle({
|
|
key: "SiteCircles",
|
|
display: "Distance Circles",
|
|
container: "#settingsRight",
|
|
init: SiteCircles,
|
|
setState: function(state) {
|
|
SiteCircles = state;
|
|
if (loadFinished)
|
|
initSitePos();
|
|
}
|
|
});
|
|
*/
|
|
|
|
new Toggle({
|
|
key: "updateLocation",
|
|
display: "Update GPS location",
|
|
container: "#settingsRight",
|
|
init: updateLocation,
|
|
setState: function(state) {
|
|
updateLocation = state;
|
|
watchPosition();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "autoselect",
|
|
display: "Auto-select plane",
|
|
container: "#settingsRight",
|
|
init: autoselect,
|
|
setState: function(state) {
|
|
autoselect = state;
|
|
setAutoselect();
|
|
}
|
|
});
|
|
if (usp.has('autoselect')) {
|
|
autoselect = true;
|
|
setAutoselect();
|
|
}
|
|
|
|
new Toggle({
|
|
key: "ColoredPlanes",
|
|
display: "Colored Planes",
|
|
container: "#settingsRight",
|
|
init: true,
|
|
setState: function(state) {
|
|
if (state)
|
|
monochromeMarkers = null;
|
|
else
|
|
monochromeMarkers = "#EEEEEE";
|
|
|
|
refreshFeatures();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "ColoredTrails",
|
|
display: "Colored Trails",
|
|
container: "#settingsRight",
|
|
init: true,
|
|
setState: function(state) {
|
|
if (state)
|
|
monochromeTracks = null;
|
|
else
|
|
monochromeTracks = "#000000";
|
|
|
|
remakeTrails();
|
|
}
|
|
});
|
|
|
|
if (globeIndex) {
|
|
function setGlobeTableLimit() {
|
|
let mult = 1 + 4 * toggles['moreTableLines1'].state + 16 * (toggles['moreTableLines2'] && toggles['moreTableLines2'].state);
|
|
globeTableLimit = globeTableLimitBase * mult;
|
|
if (toggles['allTableLines'] && toggles['allTableLines'].state)
|
|
globeTableLimit = 1e9;
|
|
if (onMobile)
|
|
globeTableLimit /= 2;
|
|
};
|
|
new Toggle({
|
|
key: "moreTableLines1",
|
|
display: "More Table Lines",
|
|
container: "#sidebar-table",
|
|
init: false,
|
|
setState: setGlobeTableLimit,
|
|
});
|
|
new Toggle({
|
|
key: "moreTableLines2",
|
|
display: "Even More Table Lines",
|
|
container: "#sidebar-table",
|
|
init: false,
|
|
setState: setGlobeTableLimit,
|
|
});
|
|
new Toggle({
|
|
key: "allTableLines",
|
|
display: "All Table Lines",
|
|
container: "#sidebar-table",
|
|
init: false,
|
|
setState: setGlobeTableLimit,
|
|
});
|
|
}
|
|
|
|
new Toggle({
|
|
key: "sidebar_visible",
|
|
display: "Sidebar visible",
|
|
container: null,
|
|
checkbox: null,
|
|
button: '#toggle_sidebar_button',
|
|
init: (onMobile ? false : true),
|
|
setState: function (state) {
|
|
if (state) {
|
|
jQuery("#sidebar_container").show();
|
|
jQuery("#expand_sidebar_control").show();
|
|
jQuery("#toggle_sidebar_button").removeClass("show_sidebar");
|
|
jQuery("#toggle_sidebar_button").addClass("hide_sidebar");
|
|
} else {
|
|
jQuery("#sidebar_container").hide();
|
|
jQuery("#expand_sidebar_control").hide();
|
|
jQuery("#toggle_sidebar_button").removeClass("hide_sidebar");
|
|
jQuery("#toggle_sidebar_button").addClass("show_sidebar");
|
|
}
|
|
updateMapSize();
|
|
},
|
|
});
|
|
|
|
if (!showPictures) {
|
|
planespottingAPI = false;
|
|
planespottersAPI = false;
|
|
}
|
|
new Toggle({
|
|
key: "planespottingAPI",
|
|
display: "Pictures planespotting.be",
|
|
container: "#settingsRight",
|
|
init: planespottingAPI,
|
|
setState: function(state) {
|
|
planespottingAPI = state;
|
|
if (state) {
|
|
toggles['planespottersAPI'] && toggles['planespottersAPI'].toggle(false);
|
|
}
|
|
setPictureVisibility();
|
|
refreshSelected();
|
|
}
|
|
});
|
|
new Toggle({
|
|
key: "planespottersAPI",
|
|
display: "Pictures planespotters.net",
|
|
container: "#settingsRight",
|
|
init: planespottersAPI,
|
|
setState: function(state) {
|
|
planespottersAPI = state;
|
|
if (state) {
|
|
toggles['planespottingAPI'] && toggles['planespottingAPI'].toggle(false);
|
|
}
|
|
setPictureVisibility();
|
|
refreshSelected();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "enableInfoblock",
|
|
display: "Enable Infoblock",
|
|
container: "#settingsRight",
|
|
init: true,
|
|
setState: function(state) {
|
|
adjustInfoBlock();
|
|
}
|
|
});
|
|
|
|
new Toggle({
|
|
key: "wideInfoblock",
|
|
display: "Wide Infoblock",
|
|
container: "#settingsRight",
|
|
init: wideInfoBlock,
|
|
setState: function(state) {
|
|
wideInfoBlock = state;
|
|
adjustInfoBlock();
|
|
}
|
|
});
|
|
|
|
|
|
jQuery('#selectall_checkbox').on('click', function() {
|
|
if (jQuery('#selectall_checkbox').hasClass('settingsCheckboxChecked')) {
|
|
deselectAllPlanes();
|
|
} else {
|
|
selectAllPlanes();
|
|
}
|
|
})
|
|
|
|
// Force map to redraw if sidebar container is resized - use a timer to debounce
|
|
jQuery("#sidebar_container").on("resize", function() {
|
|
clearTimeout(mapResizeTimeout);
|
|
mapResizeTimeout = setTimeout(updateMapSize, 20);
|
|
});
|
|
|
|
filterGroundVehicles(false);
|
|
filterBlockedMLAT(false);
|
|
|
|
TAR.altitudeChart.init();
|
|
|
|
if (adsbexchange) {
|
|
jQuery('#adsbexchange_header').show();
|
|
jQuery('#credits').show();
|
|
if (!onMobile) {
|
|
jQuery('#creditsSelected').show();
|
|
}
|
|
jQuery('#selected_infoblock').addClass('adsbx-selected-bg');
|
|
if (false && window.self != window.top) {
|
|
window.top.location.href = "https://www.adsbexchange.com/";
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function initLegend(colors) {
|
|
let html = '';
|
|
html += '<div class="legendTitle" style="background-color:' + colors['adsb'] + ';">ADS-B</div>';
|
|
if (!globeIndex)
|
|
html += '<div class="legendTitle" style="background-color:' + colors['uat'] + ';">UAT / ADS-R</div>';
|
|
if (globeIndex)
|
|
html += '<div class="legendTitle" style="background-color:' + colors['uat'] + ';">ADS-C/R / UAT</div>';
|
|
html += '<div class="legendTitle" style="background-color:' + colors['mlat'] + ';">MLAT</div>';
|
|
html += '<br>';
|
|
html += '<div class="legendTitle" style="background-color:' + colors['tisb'] + ';">TIS-B</div>';
|
|
if (!globeIndex)
|
|
html += '<div class="legendTitle" style="background-color:' + colors['modeS'] + ';">Mode-S</div>';
|
|
if (globeIndex)
|
|
html += '<div class="legendTitle" style="background-color:' + colors['other'] + ';">Other</div>';
|
|
|
|
document.getElementById('legend').innerHTML = html;
|
|
}
|
|
|
|
function initSourceFilter(colors) {
|
|
const createFilter = function (color, text, key) {
|
|
return '<li class="ui-widget-content" style="background-color:' + color + ';" id="source-filter-' + key + '">' + text + '</li>';
|
|
};
|
|
|
|
let html = '';
|
|
html += createFilter(colors['adsb'], 'ADS-B', sources[0]);
|
|
|
|
html += createFilter(colors['uat'], 'UAT / ADS-R', sources[1][0]);
|
|
html += createFilter(colors['mlat'], 'MLAT', sources[2]);
|
|
html += createFilter(colors['tisb'], 'TIS-B', sources[3]);
|
|
|
|
//if (!globeIndex)
|
|
html += createFilter(colors['modeS'], 'Mode-S', sources[4]);
|
|
if (globeIndex)
|
|
html += createFilter(colors['other'], 'Other', sources[5]);
|
|
|
|
if (globeIndex)
|
|
html += createFilter(colors['uat'], 'ADS-C', sources[6]);
|
|
|
|
document.getElementById('sourceFilter').innerHTML = html;
|
|
|
|
jQuery("#sourceFilter").selectable({
|
|
stop: function () {
|
|
sourcesFilter = [];
|
|
jQuery(".ui-selected", this).each(function () {
|
|
const index = jQuery("#sourceFilter li").index(this);
|
|
if (Array.isArray(sources[index]))
|
|
sources[index].forEach(member => { sourcesFilter.push(member); });
|
|
else
|
|
sourcesFilter.push(sources[index]);
|
|
});
|
|
}
|
|
});
|
|
|
|
jQuery("#sourceFilter").on("selectablestart", function (event, ui) {
|
|
event.originalEvent.ctrlKey = true;
|
|
});
|
|
}
|
|
|
|
function initFlagFilter(colors) {
|
|
const createFilter = function (color, text, key) {
|
|
return '<li class="ui-widget-content" style="background-color:' + color + ';" id="flag-filter-' + key + '">' + text + '</li>';
|
|
};
|
|
|
|
let html = '';
|
|
html += createFilter(colors['tisb'], 'Military', flagFilterValues[0]);
|
|
//html += createFilter(colors['mlat'], 'Interesting');
|
|
html += createFilter(colors['uat'], 'PIA', flagFilterValues[1]);
|
|
html += createFilter(colors['adsb'], 'LADD', flagFilterValues[2]);
|
|
|
|
document.getElementById('flagFilter').innerHTML = html;
|
|
|
|
jQuery("#flagFilter").selectable({
|
|
stop: function () {
|
|
flagFilter = [];
|
|
jQuery(".ui-selected", this).each(function () {
|
|
const index = jQuery("#flagFilter li").index(this);
|
|
if (Array.isArray(flagFilterValues[index]))
|
|
flagFilterValues[index].forEach(member => { flagFilter.push(member); });
|
|
else
|
|
flagFilter.push(flagFilterValues[index]);
|
|
});
|
|
}
|
|
});
|
|
|
|
jQuery("#flagFilter").on("selectablestart", function (event, ui) {
|
|
event.originalEvent.ctrlKey = true;
|
|
});
|
|
}
|
|
|
|
function initFilters() {
|
|
new Filter({
|
|
key: 'flight',
|
|
name: 'Callsign',
|
|
container: "#filterTable",
|
|
});
|
|
|
|
initSourceFilter(tableColors.unselected);
|
|
initFlagFilter(tableColors.unselected);
|
|
|
|
if (PlaneFilter) {
|
|
if (PlaneFilter.minAltitude && PlaneFilter.minAltitude > -1000000) {
|
|
jQuery('#altitude_filter_min').val(PlaneFilter.minAltitude);
|
|
}
|
|
if (PlaneFilter.maxAltitude && PlaneFilter.maxAltitude < 1000000) {
|
|
jQuery('#altitude_filter_max').val(PlaneFilter.maxAltitude);
|
|
}
|
|
|
|
if (PlaneFilter.callsign) {
|
|
jQuery('#callsign_filter').val(PlaneFilter.callsign);
|
|
}
|
|
if (PlaneFilter.type) {
|
|
jQuery('#type_filter').val(PlaneFilter.type);
|
|
}
|
|
if (PlaneFilter.description) {
|
|
jQuery('#description_filter').val(PlaneFilter.description);
|
|
}
|
|
if (PlaneFilter.icao) {
|
|
jQuery('#icao_filter').val(PlaneFilter.icao);
|
|
}
|
|
|
|
if (PlaneFilter.sources) {
|
|
sourcesFilter = PlaneFilter.sources
|
|
sourcesFilter.map((f) => jQuery('#source-filter-' + f).addClass('ui-selected'))
|
|
}
|
|
|
|
if (PlaneFilter.flagFilter) {
|
|
flagFilter = PlaneFilter.flagFilter
|
|
flagFilter.map((f) => jQuery('#flag-filter-' + f).addClass('ui-selected'))
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
function push_history() {
|
|
jQuery("#loader_progress").attr('max',nHistoryItems*2);
|
|
for (let i = 0; i < nHistoryItems; i++) {
|
|
push_history_item(i);
|
|
}
|
|
if (globeIndex) {
|
|
parseHistory();
|
|
} else if (!nHistoryItems) {
|
|
parseHistory();
|
|
console.log("History loading failed");
|
|
}
|
|
}
|
|
|
|
function push_history_item(i) {
|
|
|
|
jQuery.when(deferHistory[i])
|
|
.done(function(json) {
|
|
|
|
if (HistoryChunks) {
|
|
if (json && json.files) {
|
|
for (let i in json.files) {
|
|
PositionHistoryBuffer.push(json.files[i]);
|
|
}
|
|
} else if (json && json.now) {
|
|
PositionHistoryBuffer.push(json);
|
|
}
|
|
} else {
|
|
PositionHistoryBuffer.push(json);
|
|
}
|
|
|
|
|
|
jQuery("#loader_progress").attr('value',HistoryItemsReturned);
|
|
HistoryItemsReturned++;
|
|
if (HistoryItemsReturned == nHistoryItems) {
|
|
parseHistory();
|
|
}
|
|
})
|
|
|
|
.fail(function(jqxhr, status, error) {
|
|
|
|
//Doesn't matter if it failed, we'll just be missing a data point
|
|
jQuery("#loader_progress").attr('value',HistoryItemsReturned);
|
|
//console.log(error);
|
|
HistoryItemsReturned++;
|
|
if (HistoryItemsReturned == nHistoryItems) {
|
|
parseHistory();
|
|
}
|
|
});
|
|
}
|
|
|
|
function parseHistory() {
|
|
if (nHistoryItems) {
|
|
console.timeEnd("Downloaded History");
|
|
console.time("Loaded aircraft tracks from History");
|
|
}
|
|
|
|
for (let i in deferHistory)
|
|
deferHistory[i] = null;
|
|
|
|
|
|
if (PositionHistoryBuffer.length > 0) {
|
|
|
|
// Sort history by timestamp
|
|
console.log("Sorting history: " + PositionHistoryBuffer.length);
|
|
PositionHistoryBuffer.sort(function(x,y) { return (y.now - x.now); });
|
|
|
|
// Process history
|
|
let data;
|
|
let h = 0;
|
|
let pruneInt = Math.floor(PositionHistoryBuffer.length/5);
|
|
let currentTime = new Date().getTime()/1000;
|
|
while (data = PositionHistoryBuffer.pop()) {
|
|
|
|
if (pTracks && currentTime - data.now > pTracks * 3600) {
|
|
continue;
|
|
}
|
|
|
|
// process new data
|
|
if (PositionHistoryBuffer.length < 10) {
|
|
processReceiverUpdate(data, false);
|
|
} else {
|
|
processReceiverUpdate(data, true);
|
|
}
|
|
|
|
if (h==1) {
|
|
console.log("Applied history " + h + " from: "
|
|
+ (new Date(now * 1000)).toLocaleTimeString());
|
|
}
|
|
|
|
// prune aircraft list
|
|
if (h++ % pruneInt == pruneInt - 1) {
|
|
|
|
console.log("Applied history " + h + " from: "
|
|
+ (new Date(now * 1000)).toLocaleTimeString());
|
|
|
|
reaper();
|
|
}
|
|
}
|
|
|
|
// Final pass to update all planes to their latest state
|
|
console.log("Final history cleanup pass");
|
|
for (let i in PlanesOrdered) {
|
|
let plane = PlanesOrdered[i];
|
|
|
|
if (plane.position && SitePosition)
|
|
plane.sitedist = ol.sphere.getDistance(SitePosition, plane.position);
|
|
|
|
if (uatNoTISB && plane.uat && plane.type && plane.type.substring(0,4) == "tisb") {
|
|
plane.last_message_time -= 999;
|
|
}
|
|
}
|
|
|
|
refreshFeatures();
|
|
TAR.planeMan.refresh();
|
|
}
|
|
|
|
PositionHistoryBuffer = null;
|
|
|
|
if (nHistoryItems)
|
|
console.timeEnd("Loaded aircraft tracks from History");
|
|
|
|
historyLoaded.resolve();
|
|
}
|
|
|
|
let replay_was_active = false;
|
|
let timers = {};
|
|
let timersActive = false;
|
|
function clearIntervalTimers() {
|
|
if (!timersActive) {
|
|
return;
|
|
}
|
|
|
|
timersActive = false;
|
|
|
|
if (loadFinished) {
|
|
jQuery("#timers_paused_detail").text('Timers paused (tab hidden).');
|
|
jQuery("#timers_paused").css('display','block');
|
|
|
|
replay_was_active = replay.playing;
|
|
if (replay.playing) {
|
|
playReplay(false);
|
|
}
|
|
}
|
|
console.log("clear timers");
|
|
const entries = Object.entries(timers);
|
|
for (let i in entries) {
|
|
clearInterval(entries[i][1]);
|
|
}
|
|
|
|
}
|
|
|
|
function setIntervalTimers() {
|
|
if (timersActive) {
|
|
return;
|
|
}
|
|
|
|
timersActive = true;
|
|
|
|
if (loadFinished) {
|
|
jQuery("#timers_paused").css('display','none');
|
|
}
|
|
console.log("set timers");
|
|
if ((adsbexchange || dynGlobeRate) && !uuid) {
|
|
timers.globeRateUpdate = setInterval(globeRateUpdate, 180000);
|
|
}
|
|
pollPositionInterval();
|
|
setAutoselect();
|
|
|
|
timers.checkMove = setInterval(checkMovement, 50);
|
|
timers.everySecond = setInterval(everySecond, 850);
|
|
timers.reaper = setInterval(reaper, 40000);
|
|
//reaper();
|
|
if (tempTrails) {
|
|
timers.trailReaper = window.setInterval(trailReaper, 10000);
|
|
trailReaper(now);
|
|
}
|
|
if (enable_pf_data && !pTracks && !globeIndex) {
|
|
jQuery('#pf_info_contianer').removeClass('hidden');
|
|
timers.pf_data = window.setInterval(fetchPfData, RefreshInterval*10.314);
|
|
fetchPfData();
|
|
}
|
|
if (receiverJson && receiverJson.outlineJson) {
|
|
timers.drawOutline = window.setInterval(drawOutlineJson, 15000);
|
|
drawOutlineJson();
|
|
}
|
|
|
|
|
|
if (replay_was_active) {
|
|
playReplay(true);
|
|
}
|
|
}
|
|
|
|
let djson;
|
|
let dstring;
|
|
let dresult;
|
|
|
|
function startPage() {
|
|
|
|
|
|
if (0) {
|
|
console.log('bla');
|
|
jQuery.ajax({
|
|
url: 'data/aircraft.json.zst' , method: 'GET',
|
|
xhr: arraybufferRequest,
|
|
timeout: 15000,
|
|
}).done(function(data) {
|
|
console.log('asdf');
|
|
let arr = new Uint8Array(data);
|
|
let uncompressedSize = 83843;
|
|
console.time("zstd");
|
|
dresult = zstdDecode( arr, 0 );
|
|
console.timeEnd("zstd");
|
|
console.time("stringify");
|
|
dstring = String.fromCharCode.apply(null, dresult);
|
|
console.timeEnd("stringify");
|
|
console.time("JSON.parse");
|
|
djson = JSON.parse(dstring);
|
|
console.timeEnd("JSON.parse");
|
|
});
|
|
}
|
|
|
|
console.log("Completing init");
|
|
|
|
if (!globeIndex) {
|
|
jQuery("#lastLeg_cb").parent().hide();
|
|
jQuery('#show_trace').hide();
|
|
}
|
|
if (globeIndex && !icaoFilter) {
|
|
jQuery('#V').hide();
|
|
toggleTableInView(true);
|
|
}
|
|
|
|
if (hideButtons) {
|
|
jQuery('#header_top').hide();
|
|
jQuery('#header_side').hide();
|
|
jQuery('#splitter').hide();
|
|
jQuery('#tabs').hide();
|
|
jQuery('#filterButton').hide();
|
|
jQuery('.ol-control').hide();
|
|
jQuery('.ol-attribution').show();
|
|
}
|
|
|
|
|
|
changeZoom("init");
|
|
changeCenter("init");
|
|
|
|
clearIntervalTimers();
|
|
setIntervalTimers();
|
|
|
|
processURLParams();
|
|
if (usp.has('reg')) {
|
|
let req = regIcaoDownload();
|
|
req.done(function() {
|
|
const queries = usp.get('reg').split(',');
|
|
for (let i in queries) {
|
|
let icao = regCache[queries[i].toUpperCase()];
|
|
if (icao) {
|
|
icao = icao.toLowerCase();
|
|
urlIcaos.push(icao);
|
|
}
|
|
}
|
|
processURLParams();
|
|
});
|
|
}
|
|
|
|
loadFinished = true;
|
|
|
|
// Kick off first refresh.
|
|
fetchData();
|
|
|
|
if (tempTrails)
|
|
selectAllPlanes();
|
|
|
|
if (!heatmap)
|
|
jQuery("#loader").addClass("hidden");
|
|
|
|
if (replay) {
|
|
showReplayBar();
|
|
loadReplay(replay.ts);
|
|
}
|
|
|
|
geoMag = geoMagFactory(cof2Obj());
|
|
|
|
mapRefresh();
|
|
|
|
if (heatmap) {
|
|
drawHeatmap();
|
|
}
|
|
|
|
initVisibilityChange();
|
|
|
|
if (pTracks)
|
|
setTimeout(TAR.planeMan.refresh, 10000);
|
|
}
|
|
|
|
//
|
|
// Utils begin
|
|
//
|
|
(function (global, jQuery, TAR) {
|
|
let utils = TAR.utils = TAR.utils || {};
|
|
|
|
// Make a LineString with 'points'-number points
|
|
// that is a closed circle on the sphere such that the
|
|
// great circle distance from 'center' to each point is
|
|
// 'radius' meters
|
|
utils.make_geodesic_circle = function (center, radius, points) {
|
|
const angularDistance = radius / 6378137.0;
|
|
const lon1 = center[0] * Math.PI / 180.0;
|
|
const lat1 = center[1] * Math.PI / 180.0;
|
|
|
|
let geom;
|
|
for (let i = 0; i <= points; ++i) {
|
|
const bearing = i * 2 * Math.PI / points;
|
|
|
|
let lat2 = Math.asin(Math.sin(lat1) * Math.cos(angularDistance) +
|
|
Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(bearing));
|
|
let lon2 = lon1 + Math.atan2(Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(lat1),
|
|
Math.cos(angularDistance) - Math.sin(lat1) * Math.sin(lat2));
|
|
|
|
lat2 = lat2 * 180.0 / Math.PI;
|
|
lon2 = lon2 * 180.0 / Math.PI;
|
|
|
|
if (!geom)
|
|
geom = new ol.geom.LineString([[lon2, lat2]]);
|
|
else
|
|
geom.appendCoordinate([lon2, lat2]);
|
|
}
|
|
return geom;
|
|
}
|
|
|
|
return TAR;
|
|
}(window, jQuery, TAR || {}));
|
|
//
|
|
// Utils end
|
|
//
|
|
//
|
|
|
|
function webglAddLayer() {
|
|
let success = false;
|
|
|
|
const icao = '~c0ffee';
|
|
|
|
if (icaoFilter != null) {
|
|
icaoFilter.push(icao);
|
|
}
|
|
|
|
processAircraft({hex: icao, lat: CenterLat, lon: CenterLon, type: 'tisb_other', seen: 0, seen_pos: 0,
|
|
alt_baro: 25000, });
|
|
let plane = Planes['~c0ffee'];
|
|
|
|
try {
|
|
let glStyle = {
|
|
symbol: {
|
|
symbolType: 'image',
|
|
src: 'images/sprites016.png',
|
|
size: [ 'get', 'size' ],
|
|
offset: [0, 0],
|
|
textureCoord: [ 'array',
|
|
[ 'get', 'cx' ],
|
|
[ 'get', 'cy' ],
|
|
[ 'get', 'dx' ],
|
|
[ 'get', 'dy' ]
|
|
],
|
|
color: [
|
|
'color',
|
|
[ 'get', 'r' ],
|
|
[ 'get', 'g' ],
|
|
[ 'get', 'b' ],
|
|
1
|
|
],
|
|
rotateWithView: false,
|
|
rotation: [ 'get', 'rotation' ],
|
|
},
|
|
};
|
|
if (heatmap) {
|
|
glStyle = {
|
|
symbol: {
|
|
symbolType: "circle",
|
|
size: heatmap.radius * globalScale * 2.5,
|
|
offset: [0, 0],
|
|
opacity: heatmap.alpha || 1,
|
|
color: [
|
|
'color',
|
|
[ 'get', 'r' ],
|
|
[ 'get', 'g' ],
|
|
[ 'get', 'b' ],
|
|
1
|
|
],
|
|
}
|
|
}
|
|
}
|
|
|
|
webglLayer = new ol.layer.WebGLPoints({
|
|
name: 'webglLayer',
|
|
type: 'overlay',
|
|
title: 'Aircraft pos. webGL',
|
|
source: webglFeatures,
|
|
declutter: false,
|
|
zIndex: 200,
|
|
style: glStyle,
|
|
renderBuffer: renderBuffer,
|
|
});
|
|
if (!webglLayer || !webglLayer.getRenderer())
|
|
return false;
|
|
|
|
layers.push(webglLayer);
|
|
|
|
webgl = true;
|
|
plane.visible = true;
|
|
plane.updateMarker();
|
|
OLMap.renderSync();
|
|
|
|
success = true;
|
|
} catch (error) {
|
|
try {
|
|
layers.remove(webglLayer);
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
console.error(error);
|
|
loStore['webglFailStamp'] = new Date().getTime();
|
|
success = false;
|
|
if (loStore['webgl'] == 'true')
|
|
loStore.removeItem('webgl');
|
|
}
|
|
delete Planes[plane.icao];
|
|
PlanesOrdered.splice(PlanesOrdered.indexOf(plane), 1);
|
|
plane.destroy();
|
|
|
|
if (icaoFilter != null) {
|
|
icaoFilter.pop(icao);
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
function webglInit() {
|
|
let init = true;
|
|
// if webGL failed in the last 7 days, don't even try unless people click the toggle.
|
|
if (loStore['webglFailStamp'] && Number(loStore['webglFailStamp']) + 7 * 24 * 3600 * 1000 > new Date().getTime()) {
|
|
init = false;
|
|
if (loStore['webgl'] == undefined)
|
|
console.log('webGL failed in the past 7 days, not even trying to initialize it');
|
|
}
|
|
new Toggle({
|
|
key: "webgl",
|
|
display: "WebGL",
|
|
container: "#settingsRight",
|
|
init: init,
|
|
setState: function(state) {
|
|
if (state) {
|
|
if (webglLayer) {
|
|
webgl = true;
|
|
} else {
|
|
webgl = webglAddLayer();
|
|
}
|
|
|
|
if (!webgl) {
|
|
console.error('Unable to initialize the webGL Layer! Falling back to non-webGL icons, performance will be reduced significantly!');
|
|
webglLayer = null;
|
|
}
|
|
if (!webgl)
|
|
return false;
|
|
// returning false means the toggle will flip back as the activation of the webgl layer was unsuccessful.
|
|
} else {
|
|
webgl = false;
|
|
if (loadFinished) {
|
|
webglFeatures && webglFeatures.clear();
|
|
for (let i in PlanesOrdered) {
|
|
const plane = PlanesOrdered[i];
|
|
delete plane.glMarker;
|
|
}
|
|
}
|
|
}
|
|
if (loadFinished) {
|
|
refreshFilter();
|
|
checkPointermove();
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
// Initalizes the map and starts up our timers to call various functions
|
|
function initMap() {
|
|
|
|
if (globeIndex) {
|
|
jQuery('#dump1090_total_history_td').hide();
|
|
jQuery('#dump1090_message_rate_td').hide();
|
|
}
|
|
|
|
// Load stored map settings if present
|
|
CenterLon = Number(loStore['CenterLon']) || DefaultCenterLon;
|
|
CenterLat = Number(loStore['CenterLat']) || DefaultCenterLat;
|
|
zoomLvl = Number(loStore['zoomLvl']) || DefaultZoomLvl;
|
|
zoomLvlCache = zoomLvl;
|
|
|
|
if (overrideMapType)
|
|
MapType_tar1090 = overrideMapType;
|
|
else if (loStore['MapType_tar1090']) {
|
|
MapType_tar1090 = loStore['MapType_tar1090'];
|
|
}
|
|
|
|
mapTypeSettings();
|
|
|
|
// Initialize OpenLayers
|
|
|
|
layers_group = createBaseLayers();
|
|
layers = layers_group.getLayers();
|
|
|
|
//add_kml_overlay('https://developers.google.com/kml/documentation/KML_Samples.kml', 'samples', 0.8);
|
|
|
|
siteCircleLayer = new ol.layer.Vector({
|
|
name: 'siteCircles',
|
|
type: 'overlay',
|
|
title: 'Range rings',
|
|
source: siteCircleFeatures,
|
|
visible: SiteCircles,
|
|
zIndex: 100,
|
|
renderOrder: null,
|
|
renderBuffer: renderBuffer,
|
|
});
|
|
layers.push(siteCircleLayer);
|
|
|
|
siteCircleLayer.on('change:visible', function(evt) {
|
|
if (evt.target.getVisible()) {
|
|
geoFindMe();
|
|
}
|
|
});
|
|
|
|
locationDotLayer = new ol.layer.Vector({
|
|
name: 'locationDot',
|
|
type: 'overlay',
|
|
title: (receiverJson && receiverJson.lat != null) ? 'Site position' : 'Your position',
|
|
source: locationDotFeatures,
|
|
visible: SiteShow,
|
|
zIndex: 100,
|
|
renderOrder: null,
|
|
renderBuffer: renderBuffer,
|
|
});
|
|
layers.push(locationDotLayer);
|
|
|
|
locationDotLayer.on('change:visible', function(evt) {
|
|
if (evt.target.getVisible()) {
|
|
geoFindMe();
|
|
}
|
|
});
|
|
|
|
|
|
if (receiverJson && receiverJson.outlineJson) {
|
|
actualOutlineFeatures = new ol.source.Vector();
|
|
actualOutlineStyle = new ol.style.Style({
|
|
fill: null,
|
|
stroke: new ol.style.Stroke({
|
|
color: actual_range_outline_color,
|
|
width: actual_range_outline_width,
|
|
lineDash: actual_range_outline_dash,
|
|
}),
|
|
});
|
|
actualOutlineLayer = new ol.layer.Vector({
|
|
name: 'actualRangeOutline',
|
|
type: 'overlay',
|
|
title: 'actual range outline',
|
|
source: actualOutlineFeatures,
|
|
zIndex: 101,
|
|
renderBuffer: renderBuffer,
|
|
style: actualOutlineStyle,
|
|
visible: actual_range_show,
|
|
});
|
|
layers.push(actualOutlineLayer);
|
|
}
|
|
if (calcOutlineData) {
|
|
calcOutlineLayer = new ol.layer.Vector({
|
|
name: 'calcOutline',
|
|
type: 'overlay',
|
|
title: 'terrain-based range outline',
|
|
source: calcOutlineFeatures,
|
|
zIndex: 100,
|
|
renderOrder: null,
|
|
renderBuffer: renderBuffer,
|
|
});
|
|
layers.push(calcOutlineLayer);
|
|
drawUpintheair();
|
|
}
|
|
|
|
|
|
const dummyLayer = new ol.layer.Vector({
|
|
name: 'dummy',
|
|
renderOrder: null,
|
|
});
|
|
|
|
trailGroup.push(dummyLayer);
|
|
|
|
trailLayers = new ol.layer.Group({
|
|
name: 'ac_trail',
|
|
title: 'Aircraft trails',
|
|
type: 'overlay',
|
|
layers: trailGroup,
|
|
zIndex: 150,
|
|
});
|
|
|
|
layers.push(trailLayers);
|
|
|
|
iconLayer = new ol.layer.Vector({
|
|
name: 'iconLayer',
|
|
type: 'overlay',
|
|
title: 'Aircraft positions',
|
|
source: PlaneIconFeatures,
|
|
declutter: false,
|
|
zIndex: 200,
|
|
renderBuffer: renderBuffer,
|
|
});
|
|
layers.push(iconLayer);
|
|
|
|
|
|
OLMap = new ol.Map({
|
|
target: 'map_canvas',
|
|
layers: layers,
|
|
view: new ol.View({
|
|
center: ol.proj.fromLonLat([CenterLon, CenterLat]),
|
|
zoom: zoomLvl,
|
|
multiWorld: true,
|
|
}),
|
|
controls: [new ol.control.Zoom({delta: 1, duration: 0, target: 'map_canvas',}),
|
|
new ol.control.Attribution({collapsed: true}),
|
|
new ol.control.ScaleLine({units: DisplayUnits})
|
|
],
|
|
interactions: new ol.interaction.defaults({altShiftDragRotate:false, pinchRotate:false,}),
|
|
});
|
|
|
|
console.time('webglInit');
|
|
webglInit();
|
|
console.timeEnd('webglInit');
|
|
|
|
let foundType = false;
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (lyr.get('name') && lyr.get('type') == 'base') {
|
|
if (MapType_tar1090 == lyr.get('name')) {
|
|
foundType = true;
|
|
}
|
|
}
|
|
});
|
|
if (!foundType) {
|
|
MapType_tar1090 = "osm_adsbx";
|
|
}
|
|
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (!lyr.get('name'))
|
|
return;
|
|
|
|
if (lyr.get('type') == 'base') {
|
|
if (MapType_tar1090 == lyr.get('name')) {
|
|
foundType = true;
|
|
lyr.setVisible(true);
|
|
} else {
|
|
lyr.setVisible(false);
|
|
}
|
|
|
|
lyr.on('change:visible', function(evt) {
|
|
if (evt.target.getVisible()) {
|
|
MapType_tar1090 = loStore['MapType_tar1090'] = evt.target.get('name');
|
|
mapTypeSettings();
|
|
}
|
|
});
|
|
} else if (lyr.get('type') === 'overlay') {
|
|
if (
|
|
loStore['layer_' + lyr.get('name')] == 'true'
|
|
|| enableOverlays.indexOf(lyr.get('name')) >= 0
|
|
|| (loStore['layer_' + lyr.get('name')] != 'false' && defaultOverlays.indexOf(lyr.get('name')) >= 0)
|
|
) {
|
|
lyr.setVisible(true);
|
|
}
|
|
if (loStore['layer_' + lyr.get('name')] == 'false') {
|
|
lyr.setVisible(false);
|
|
}
|
|
|
|
lyr.on('change:visible', function(evt) {
|
|
loStore['layer_' + evt.target.get('name')] = evt.target.getVisible();
|
|
});
|
|
}
|
|
})
|
|
|
|
if (!foundType) {
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (foundType)
|
|
return;
|
|
if (lyr.get('type') === 'base') {
|
|
lyr.setVisible(true);
|
|
foundType = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
OLProj = OLMap.getView().getProjection();
|
|
OLProjExtent = OLProj.getExtent();
|
|
|
|
OLMap.getView().setRotation(mapOrientation); // adjust orientation
|
|
|
|
OLMap.addControl(new ol.control.LayerSwitcher({
|
|
groupSelectStyle: 'none',
|
|
activationMode: 'click', // click sucks in the current implementation
|
|
target: 'map_canvas',
|
|
}));
|
|
|
|
OLMap.on('moveend', function(event) {
|
|
checkMovement();
|
|
});
|
|
|
|
OLMap.on(['click', 'dblclick'], function(evt) {
|
|
let trailHex = null;
|
|
let trailTS = null;
|
|
let planeHex = null;
|
|
|
|
let features = webgl ? webglFeatures : PlaneIconFeatures;
|
|
let evtCoords = evt.map.getCoordinateFromPixel(evt.pixel);
|
|
let feature = features.getClosestFeatureToCoordinate(evtCoords);
|
|
if (feature) {
|
|
let fPixel = evt.map.getPixelFromCoordinate(feature.getGeometry().getCoordinates());
|
|
let a = fPixel[0] - evt.pixel[0];
|
|
let b = fPixel[1] - evt.pixel[1];
|
|
let c = globalScale * (onMobile ? 30 : 20);
|
|
if (a**2 + b**2 < c**2)
|
|
planeHex = feature.hex;
|
|
}
|
|
|
|
if (!planeHex || showTrace) {
|
|
let features = evt.map.getFeaturesAtPixel(
|
|
evt.pixel,
|
|
{
|
|
layerFilter: function(layer) { return (layer.get('isTrail') == true); },
|
|
hitTolerance: globalScale * (onMobile ? 30 : 20),
|
|
}
|
|
);
|
|
if (features.length > 0) {
|
|
let close = 10000000000000;
|
|
let closest = features[0];
|
|
for (let j in features) {
|
|
let feature = features[j];
|
|
let coords;
|
|
if (feature.isLabel)
|
|
coords = [feature.getGeometry().getCoordinates()];
|
|
else
|
|
coords = feature.getGeometry().getCoordinates();
|
|
|
|
for (let k in coords) {
|
|
let fPixel = evt.map.getPixelFromCoordinate(coords[k]);
|
|
let a = fPixel[0] - evt.pixel[0];
|
|
let b = fPixel[1] - evt.pixel[1];
|
|
let distance = a**2 + b**2;
|
|
if (distance < close) {
|
|
closest = feature;
|
|
close = distance;
|
|
}
|
|
}
|
|
}
|
|
if (showTrace)
|
|
trailTS = closest.timestamp;
|
|
else
|
|
trailHex = closest.hex;
|
|
}
|
|
}
|
|
|
|
const dblclick = (evt.type === 'dblclick') && !showTrace;
|
|
|
|
if (showTrace && trailTS) {
|
|
gotoTime(trailTS);
|
|
}
|
|
let hex = planeHex || trailHex;
|
|
if (hex) {
|
|
selectPlaneByHex(hex, {noDeselect: dblclick, follow: dblclick});
|
|
}
|
|
|
|
if (!hex && !multiSelect && !showTrace) {
|
|
if (onlySelected)
|
|
toggleIsolation();
|
|
deselect(SelectedPlane);
|
|
refreshFilter();
|
|
}
|
|
evt.stopPropagation();
|
|
});
|
|
|
|
jQuery('#infoblock_close').on('click', function () {
|
|
|
|
if (showTrace)
|
|
toggleShowTrace();
|
|
if (onlySelected)
|
|
toggleIsolation();
|
|
|
|
deselect(SelectedPlane);
|
|
refreshFilter();
|
|
});
|
|
|
|
|
|
// show the hover box
|
|
if (!globeIndex && zoomLvl > 5.5 && enableMouseover) {
|
|
OLMap.on('pointermove', onPointermove);
|
|
}
|
|
|
|
// handle the layer settings pane checkboxes
|
|
OLMap.once('postrender', function(e) {
|
|
//toggleLayer('#nexrad_checkbox', 'nexrad');
|
|
//toggleLayer('#sitepos_checkbox', 'site_pos');
|
|
//toggleLayer('#actrail_checkbox', 'ac_trail');
|
|
//toggleLayer('#acpositions_checkbox', 'webglLayer');
|
|
});
|
|
|
|
new Toggle({
|
|
key: "darkerColors",
|
|
display: "Darker Colors",
|
|
container: "#settingsLeft",
|
|
init: darkerColors,
|
|
setState: function(state) {
|
|
darkerColors = state;
|
|
if (loadFinished) {
|
|
refreshFeatures();
|
|
remakeTrails();
|
|
}
|
|
}
|
|
});
|
|
|
|
tableColorsLight = tableColors;
|
|
tableColorsDark = JSON.parse(JSON.stringify(tableColors));
|
|
let darkVals = Object.values(tableColorsDark);
|
|
for (let i in ['selected', 'unselected']) {
|
|
let obj = darkVals[i];
|
|
let keys = Object.keys(obj)
|
|
for (let j in keys) {
|
|
let key = keys[j];
|
|
let hsl = hexToHSL(obj[key]);
|
|
hsl[1] *= 0.4;
|
|
hsl[2] *= 0.3;
|
|
obj[key] = hslToRgb(hsl);
|
|
}
|
|
}
|
|
new Toggle({
|
|
key: "darkMode",
|
|
display: "Dark Mode",
|
|
container: "#settingsLeft",
|
|
init: darkModeDefault,
|
|
setState: function(state) {
|
|
let root = document.documentElement;
|
|
jQuery(".layer-switcher .panel").css("background", "var(--BGCOLOR1)");
|
|
jQuery(".layer-switcher .panel").css("border", "4px solid var(--BGCOLOR1)");
|
|
if (state) {
|
|
root.style.setProperty("--BGCOLOR1", '#313131');
|
|
root.style.setProperty("--BGCOLOR2", '#242424');
|
|
root.style.setProperty("--TXTCOLOR1","#BFBFBF");
|
|
root.style.setProperty("--TXTCOLOR2","#D8D8D8");
|
|
root.style.setProperty("--TXTCOLOR3","#a8a8a8");
|
|
//invert the "x" images
|
|
jQuery(".infoblockCloseBox").css('filter','invert(100%)');
|
|
jQuery(".infoblockCloseBox").css(' -webkit-filter','invert(100%)');
|
|
jQuery(".settingsCloseBox").css('filter','invert(100%)');
|
|
jQuery(".settingsCloseBox").css(' -webkit-filter','invert(100%)');
|
|
tableColors = tableColorsDark;
|
|
} else {
|
|
root.style.setProperty("--BGCOLOR1", '#F8F8F8');
|
|
root.style.setProperty("--BGCOLOR2", '#CCCCCC');
|
|
root.style.setProperty("--TXTCOLOR1","#003f4b");
|
|
root.style.setProperty("--TXTCOLOR2","#050505");
|
|
root.style.setProperty("--TXTCOLOR3","#003f4b");
|
|
jQuery(".infoblockCloseBox").css('filter','invert(0%)');
|
|
jQuery(".infoblockCloseBox").css(' -webkit-filter','invert(0%)');
|
|
jQuery(".settingsCloseBox").css('filter','invert(0%)');
|
|
jQuery(".settingsCloseBox").css(' -webkit-filter','invert(0%)');
|
|
|
|
tableColors = tableColorsLight;
|
|
}
|
|
if (loadFinished) {
|
|
TAR.planeMan.redraw();
|
|
refreshFilter();
|
|
initLegend(tableColors.unselected);
|
|
initSourceFilter(tableColors.unselected);
|
|
initFlagFilter(tableColors.unselected);
|
|
}
|
|
}
|
|
});
|
|
|
|
initLegend(tableColors.unselected);
|
|
|
|
initFilters();
|
|
|
|
new Toggle({
|
|
key: "MapDim",
|
|
display: "Dim Map",
|
|
container: "#settingsLeft",
|
|
init: MapDim,
|
|
setState: function(state) {
|
|
if (!state) {
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (lyr.get('type') != 'base')
|
|
return;
|
|
ol.Observable.unByKey(lyr.dimKey);
|
|
});
|
|
} else {
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (lyr.get('type') != 'base')
|
|
return;
|
|
lyr.dimKey = lyr.on('postrender', dim);
|
|
});
|
|
}
|
|
OLMap.render();
|
|
buttonActive('#B', state);
|
|
}
|
|
});
|
|
|
|
window.addEventListener('keydown', function(e) {
|
|
active();
|
|
if (e.defaultPrevented ) {
|
|
return; // Do nothing if the event was already processed
|
|
}
|
|
if (e.target.type == "text") {
|
|
return;
|
|
}
|
|
if (e.srcElement.nodeName == 'INPUT') {
|
|
return;
|
|
}
|
|
|
|
if( e.ctrlKey || e.altKey || e.metaKey) {
|
|
return;
|
|
}
|
|
let oldCenter, extent, newCenter;
|
|
switch (e.key) {
|
|
case "c":
|
|
case "Esc":
|
|
case "Escape":
|
|
deselectAllPlanes();
|
|
break;
|
|
// zoom and movement
|
|
case "q":
|
|
case "-":
|
|
case "Subtract":
|
|
zoomOut();
|
|
break;
|
|
case "e":
|
|
case "+":
|
|
case "Add":
|
|
zoomIn();
|
|
break;
|
|
case "ArrowUp":
|
|
case "w":
|
|
oldCenter = OLMap.getView().getCenter();
|
|
extent = OLMap.getView().calculateExtent(OLMap.getSize());
|
|
newCenter = [oldCenter[0], (oldCenter[1] + extent[3])/2];
|
|
OLMap.getView().setCenter(newCenter);
|
|
toggleFollow(false);
|
|
break;
|
|
case "ArrowDown":
|
|
case "s":
|
|
oldCenter = OLMap.getView().getCenter();
|
|
extent = OLMap.getView().calculateExtent(OLMap.getSize());
|
|
newCenter = [oldCenter[0], (oldCenter[1] + extent[1])/2];
|
|
OLMap.getView().setCenter(newCenter);
|
|
toggleFollow(false);
|
|
break;
|
|
case "ArrowLeft":
|
|
case "a":
|
|
oldCenter = OLMap.getView().getCenter();
|
|
extent = OLMap.getView().calculateExtent(OLMap.getSize());
|
|
newCenter = [(oldCenter[0] + extent[0])/2, oldCenter[1]];
|
|
OLMap.getView().setCenter(newCenter);
|
|
toggleFollow(false);
|
|
break;
|
|
case "ArrowRight":
|
|
case "d":
|
|
oldCenter = OLMap.getView().getCenter();
|
|
extent = OLMap.getView().calculateExtent(OLMap.getSize());
|
|
newCenter = [(oldCenter[0] + extent[2])/2, oldCenter[1]];
|
|
OLMap.getView().setCenter(newCenter);
|
|
toggleFollow(false);
|
|
break;
|
|
// misc
|
|
case "b":
|
|
toggles['MapDim'].toggle();
|
|
break;
|
|
case "m":
|
|
toggleMultiSelect();
|
|
break;
|
|
case "v":
|
|
toggleTableInView();
|
|
break;
|
|
case "r":
|
|
if (heatmap)
|
|
drawHeatmap();
|
|
else
|
|
followRandomPlane();
|
|
break;
|
|
case "R":
|
|
fetchData();
|
|
break;
|
|
case "t":
|
|
selectAllPlanes();
|
|
break;
|
|
case "G":
|
|
nogpsOnly = !nogpsOnly;
|
|
refreshFilter();
|
|
break;
|
|
case "h":
|
|
resetMap();
|
|
break;
|
|
case "H":
|
|
if (!hideButtons) {
|
|
jQuery('#header_top').hide();
|
|
jQuery('#header_side').hide();
|
|
jQuery('#splitter').hide();
|
|
jQuery('#tabs').hide();
|
|
jQuery('#filterButton').hide();
|
|
jQuery('.ol-control').hide();
|
|
jQuery('.ol-attribution').show();
|
|
} else {
|
|
jQuery('#header_top').show();
|
|
jQuery('#header_side').show();
|
|
jQuery('#splitter').show();
|
|
jQuery('#tabs').show();
|
|
jQuery('#filterButton').show();
|
|
jQuery('.ol-control').show();
|
|
jQuery('#expand_sidebar_control').hide();
|
|
toggles['sidebar_visible'].restore();
|
|
TAR.altitudeChart.render();
|
|
}
|
|
hideButtons = !hideButtons;
|
|
break;
|
|
case "f":
|
|
toggleFollow();
|
|
break;
|
|
// filters
|
|
case "T":
|
|
filterTISB = !filterTISB;
|
|
refreshFilter();
|
|
break;
|
|
case "u":
|
|
toggleMilitary();
|
|
break;
|
|
case "i":
|
|
toggleIsolation();
|
|
break;
|
|
// persistence mode
|
|
case "p":
|
|
togglePersistence();
|
|
break;
|
|
// Labels
|
|
case "l":
|
|
toggleLabels();
|
|
break;
|
|
case "o":
|
|
toggleExtendedLabels();
|
|
break;
|
|
case "k":
|
|
toggleTrackLabels();
|
|
break;
|
|
// debug stuff
|
|
case "L":
|
|
toggles['lastLeg'].toggle();
|
|
break;
|
|
case "D":
|
|
debug = !debug;
|
|
loStore['debug'] = debug;
|
|
console.log('debug = ' + debug);
|
|
break;
|
|
case "P":
|
|
debugPosFilter = !debugPosFilter;
|
|
loStore['debugPosFilter'] = debugPosFilter;
|
|
console.log('debugPosFilter = ' + debugPosFilter);
|
|
break;
|
|
case "?":
|
|
if (!SelectedPlane) {
|
|
console.log("No plane selected");
|
|
break;
|
|
}
|
|
console.log(SelectedPlane.icao + ": " + SelectedPlane.baseMarkerKey + " " + SelectedPlane.shape);
|
|
console.log(SelectedPlane);
|
|
console.log(SelectedPlane.milRange());
|
|
break;
|
|
case "j":
|
|
selectPlaneByHex(jumpTo, {follow: true});
|
|
break;
|
|
case "J":
|
|
debugJump = !debugJump;
|
|
loStore['debugJump'] = debugJump;
|
|
console.log('debugJump = ' + debugJump);
|
|
break;
|
|
case "N":
|
|
noMLAT = !noMLAT;
|
|
loStore['noMLAT'] = noMLAT;
|
|
console.log('noMLAT = ' + noMLAT);
|
|
break;
|
|
}
|
|
}, true);
|
|
|
|
if (!usp.has('icao')
|
|
&& !usp.has("lat") && !usp.has("lon")
|
|
&& !usp.has('airport')
|
|
) {
|
|
geoFindMe();
|
|
} else {
|
|
initSitePos();
|
|
}
|
|
}
|
|
/*
|
|
jQuery("#geoFindMeDialog").dialog({
|
|
resizable: false,
|
|
height: "auto",
|
|
width: "auto",
|
|
buttons: {
|
|
"Yes": function() {
|
|
geoFindMe();
|
|
jQuery(this).dialog( "close" );
|
|
},
|
|
"No": function() {
|
|
loStore['geoFindMeFirstVisit'] = 'no'
|
|
jQuery(this).dialog( "close" );
|
|
}
|
|
}
|
|
});
|
|
*/
|
|
|
|
// This looks for planes to reap out of the master Planes variable
|
|
let lastReap = 0;
|
|
function reaper(all) {
|
|
console.log("Reaping started..");
|
|
if (noVanish && !all)
|
|
return;
|
|
|
|
if (lastReap == "in_progress") {
|
|
return;
|
|
}
|
|
|
|
lastReap = "in_progress";
|
|
|
|
// Look for planes where we have seen no messages for >300 seconds
|
|
let plane;
|
|
let length = PlanesOrdered.length;
|
|
let temp = []
|
|
for (let i = 0; i < length; i++) {
|
|
plane = PlanesOrdered[i];
|
|
if (plane == null)
|
|
continue;
|
|
plane.seen = now - plane.last_message_time;
|
|
if ( all || ((!plane.selected)
|
|
&& plane.seen > 300
|
|
&& (plane.dataSource != 'adsc' || plane.seen > jaeroTimeout))
|
|
) {
|
|
// Reap it.
|
|
//console.log("Removed " + plane.icao);
|
|
delete Planes[plane.icao];
|
|
plane.destroy();
|
|
continue;
|
|
}
|
|
|
|
// Keep it.
|
|
temp.push(plane);
|
|
|
|
if (globeIndex) {
|
|
if (plane.clearTraceAfter) {
|
|
//console.log(now - plane.clearTraceAfter);
|
|
if (now > plane.clearTraceAfter) {
|
|
plane.clearTrace();
|
|
//console.log("clearTrace: " + plane.icao);
|
|
}
|
|
} else if (!plane.linesDrawn) {
|
|
plane.clearTraceAfter = now + 300;
|
|
}
|
|
}
|
|
}
|
|
PlanesOrdered = temp;
|
|
|
|
lastReap = now;
|
|
//console.log(length - PlanesOrdered.length);
|
|
return (length - PlanesOrdered.length);
|
|
}
|
|
|
|
// Page Title update function
|
|
function refreshPageTitle() {
|
|
if (pTracks)
|
|
return;
|
|
if (!PlaneCountInTitle && !MessageRateInTitle) {
|
|
return;
|
|
}
|
|
|
|
let subtitle = "";
|
|
|
|
if (PlaneCountInTitle) {
|
|
if (globeIndex) {
|
|
subtitle += 'tracking ' + globeTrackedAircraft + ' aircraft';
|
|
} else {
|
|
subtitle += TrackedAircraftPositions + '/' + TrackedAircraft;
|
|
}
|
|
}
|
|
|
|
if (MessageRateInTitle && MessageRate != null) {
|
|
if (subtitle) subtitle += ' | ';
|
|
subtitle += MessageRate.toFixed(1) + '/s';
|
|
}
|
|
|
|
document.title = PageName + ' - ' + subtitle;
|
|
}
|
|
|
|
function displaySil() {
|
|
jQuery('#copyrightInfo').html("");
|
|
if (!showSil) {
|
|
setPhotoHtml("");
|
|
return;
|
|
}
|
|
let selected = SelectedPlane;
|
|
let new_html="";
|
|
let type = selected.icaoType ? selected.icaoType : 'ZZZZ';
|
|
let hex = selected.icao.toUpperCase();
|
|
new_html = "<img id='silhouette' width='"+ 151 * globalScale + "' src='aircraft_sil/" + type + ".png' />";
|
|
setPhotoHtml(new_html);
|
|
selected.icao.toUpperCase();
|
|
}
|
|
|
|
function displayPhoto() {
|
|
if (!SelectedPlane)
|
|
return;
|
|
if (!SelectedPlane.psAPIresponse) {
|
|
displaySil();
|
|
return;
|
|
}
|
|
let photos = SelectedPlane.psAPIresponse["photos"] || SelectedPlane.psAPIresponse["images"];
|
|
if (!photos || photos.length == 0) {
|
|
displaySil();
|
|
adjustInfoBlock();
|
|
return;
|
|
}
|
|
let new_html="";
|
|
let photoToPull = photos[0]["thumbnail"]["src"] || photos[0]["thumbnail"];
|
|
let linkToPicture = photos[0]["link"];
|
|
//console.log(linkToPicture);
|
|
new_html = '<a class=\"link\" href="'+linkToPicture+'" target="_blank" rel="noopener noreferrer"><img id="airplanePhoto" src=' +photoToPull+'></a>';
|
|
let copyright = photos[0]["photographer"] || photos[0]["user"];
|
|
jQuery('#copyrightInfo').html("<span>Image © " + copyright +"</span>");
|
|
setPhotoHtml(new_html);
|
|
adjustInfoBlock();
|
|
}
|
|
|
|
function refreshPhoto(selected) {
|
|
if (!showPictures || selected.icao[0] == '~' || (!planespottingAPI && !planespottersAPI)) {
|
|
displaySil();
|
|
return;
|
|
}
|
|
let urlTail;
|
|
let param;
|
|
if (!selected.dbinfoLoaded) {
|
|
displaySil();
|
|
return;
|
|
} else if (false && selected.registration != null && selected.registration.match(/^[0-9]{0,2}\+?[0-9]{0,2}$/)) {
|
|
urlTail = '/hex/' + selected.icao.toUpperCase();
|
|
} else if (selected.registration != null) {
|
|
urlTail = '/hex/' + selected.icao.toUpperCase() + '?reg=' + selected.registration;
|
|
const type = selected.icaoType;
|
|
// && type != 'E170' && !type.startsWith('E75')
|
|
if (type) {
|
|
urlTail += '&icaoType=' + type;
|
|
}
|
|
param = 'DB';
|
|
} else {
|
|
urlTail = 'hex/' + selected.icao.toUpperCase();
|
|
param = 'hex';
|
|
}
|
|
|
|
|
|
const ts = new Date().getTime();
|
|
if (param == selected.psAPIparam) {
|
|
if (selected.psAPIresponse) {
|
|
displayPhoto();
|
|
return;
|
|
}
|
|
if (selected.psAPIresponseTS && selected.psAPIresponseTS - ts < 10000) {
|
|
return;
|
|
}
|
|
}
|
|
selected.psAPIparam = param;
|
|
|
|
setPhotoHtml("<p>Loading image...</p>");
|
|
jQuery('#copyrightInfo').html("<span></span>");
|
|
//console.log(ts/1000 + 'sending psAPI request');
|
|
selected.psAPIresponseTS = ts;
|
|
|
|
if (planespottersAPI) {
|
|
let req = jQuery.ajax({
|
|
url: 'https://api.planespotters.net/pub/photos/' + urlTail,
|
|
dataType: 'json',
|
|
plane: selected,
|
|
});
|
|
|
|
req.done(function(data) {
|
|
this.plane.psAPIresponse = data;
|
|
if (SelectedPlane == this.plane) {
|
|
displayPhoto();
|
|
}
|
|
});
|
|
} else if (planespottingAPI) {
|
|
let req = jQuery.ajax({
|
|
url: 'https://www.planespotting.be/api/objects/imagesRegistration.php?registration=' + selected.registration,
|
|
dataType: 'json',
|
|
plane: selected,
|
|
});
|
|
|
|
req.done(function(data) {
|
|
this.plane.psAPIresponse = data;
|
|
if (SelectedPlane == this.plane) {
|
|
displayPhoto();
|
|
}
|
|
});
|
|
req.fail(function() {
|
|
this.plane.psAPIresponse = {'photos': []};
|
|
if (SelectedPlane == this.plane) {
|
|
displayPhoto();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
let selCall = null;
|
|
let selIcao = null;
|
|
let selReg = null;
|
|
|
|
let somethingSelected = false;
|
|
// Refresh the detail window about the plane
|
|
function refreshSelected() {
|
|
const selected = SelectedPlane;
|
|
|
|
if (!selected) {
|
|
if (somethingSelected) {
|
|
adjustInfoBlock();
|
|
buttonActive('#F', FollowSelected);
|
|
}
|
|
somethingSelected = false;
|
|
return;
|
|
}
|
|
somethingSelected = true;
|
|
buttonActive('#F', FollowSelected);
|
|
|
|
selected.checkVisible();
|
|
selected.checkForDB();
|
|
|
|
refreshPhoto(selected);
|
|
|
|
jQuery('#selected_callsign').updateText(selected.name);
|
|
|
|
if (showTrace) {
|
|
if (selected.position_time) {
|
|
const date = new Date(selected.position_time * 1000);
|
|
let timestamp = utcTimesHistoric ? (zuluTime(date) + NBSP + 'Z') : (lDateString(date) + ' ' + localTime(date) + NBSP + TIMEZONE);
|
|
jQuery('#trace_time').updateText('Time:\n' + timestamp);
|
|
} else {
|
|
jQuery('#trace_time').updateText('Time:\n');
|
|
}
|
|
}
|
|
|
|
if (flightawareLinks) {
|
|
jQuery('#selected_flightaware_link').html(getFlightAwareModeSLink(selected.icao, selected.flight, "Visit Flight Page"));
|
|
}
|
|
|
|
if (selected.isNonIcao() && selected.source != 'mlat') {
|
|
jQuery('#anon_mlat_info').addClass('hidden');
|
|
jQuery('#reg_info').addClass('hidden');
|
|
jQuery('#tisb_info').removeClass('hidden');
|
|
} else if (selected.isNonIcao() && selected.source == 'mlat') {
|
|
jQuery('#reg_info').addClass('hidden');
|
|
jQuery('#tisb_info').addClass('hidden');
|
|
jQuery('#anon_mlat_info').removeClass('hidden');
|
|
} else {
|
|
jQuery('#tisb_info').addClass('hidden');
|
|
jQuery('#anon_mlat_info').addClass('hidden');
|
|
jQuery('#reg_info').removeClass('hidden');
|
|
}
|
|
|
|
let checkReg = selected.registration + ' ' + selected.dbinfoLoaded;
|
|
if (checkReg != selReg) {
|
|
selReg = checkReg;
|
|
if (selected.registration) {
|
|
if (flightawareLinks) {
|
|
jQuery('#selected_registration').html(getFlightAwareIdentLink(selected.registration, selected.registration));
|
|
} else if (registrationLinks && registrationLink(selected)) {
|
|
jQuery('#selected_registration').html(`<a class="link" target="_blank" href="${registrationLink(selected)}">${selected.registration}</a>`);
|
|
} else {
|
|
jQuery('#selected_registration').updateText(selected.registration);
|
|
}
|
|
} else {
|
|
jQuery('#selected_registration').updateText("n/a");
|
|
}
|
|
}
|
|
let dbFlags = "";
|
|
if (selected.ladd)
|
|
dbFlags += ' <a class="link" target="_blank" href="https://www.faa.gov/pilots/ladd/" rel="noreferrer">LADD</a> / ';
|
|
if (selected.pia)
|
|
dbFlags += '<a class="link" target="_blank" href="https://www.faa.gov/air_traffic/technology/equipadsb/privacy/" rel="noreferrer">PIA</a> / ';
|
|
if (selected.military)
|
|
dbFlags += 'military / ';
|
|
if (dbFlags.length == 0) {
|
|
jQuery('#selected_dbFlags').updateText("none");
|
|
} else {
|
|
jQuery('#selected_dbFlags').html(dbFlags.slice(0, -3));
|
|
}
|
|
|
|
if (selected.icaoType) {
|
|
jQuery('#selected_icaotype').updateText(selected.icaoType);
|
|
} else {
|
|
jQuery('#selected_icaotype').updateText("n/a");
|
|
}
|
|
if (selected.typeDescription)
|
|
jQuery('#selected_typedesc').updateText(selected.typeDescription);
|
|
else
|
|
jQuery('#selected_typedesc').updateText("n/a");
|
|
|
|
let typeLine = "";
|
|
if (selected.year)
|
|
typeLine += selected.year + " "
|
|
if (selected.typeLong)
|
|
typeLine += selected.typeLong;
|
|
if (!typeLine)
|
|
typeLine = "n/a"
|
|
|
|
jQuery('#selected_typelong').updateText(typeLine);
|
|
|
|
if (selected.ownOp)
|
|
jQuery('#selected_ownop').updateText(selected.ownOp);
|
|
else
|
|
jQuery('#selected_ownop').updateText("");
|
|
|
|
if (selected.rId && show_rId) {
|
|
jQuery('#receiver_id').updateText(selected.rId);
|
|
jQuery('#receiver_id_div').removeClass('hidden');
|
|
} else {
|
|
jQuery('#receiver_id_div').addClass('hidden');
|
|
}
|
|
|
|
|
|
jQuery("#selected_altitude1").updateText(format_altitude_long(selected.altitude, selected.vert_rate, DisplayUnits));
|
|
jQuery("#selected_altitude2").updateText(format_altitude_long(selected.altitude, selected.vert_rate, DisplayUnits));
|
|
|
|
jQuery('#selected_onground').updateText(format_onground(selected.altitude));
|
|
|
|
if (selected.squawk == null || selected.squawk == '0000') {
|
|
jQuery('#selected_squawk1').updateText('n/a');
|
|
jQuery('#selected_squawk2').updateText('n/a');
|
|
} else {
|
|
jQuery('#selected_squawk1').updateText(selected.squawk);
|
|
jQuery('#selected_squawk2').updateText(selected.squawk);
|
|
}
|
|
|
|
let magResult = null;
|
|
|
|
if (geoMag && selected.position != null) {
|
|
let lon = selected.position[0];
|
|
let lat = selected.position[1];
|
|
let alt = selected.altitude == "ground" ? 0 : selected.altitude;
|
|
magResult = geoMag(lat, lon, alt);
|
|
jQuery('#selected_mag_declination').updateText(format_track_brief(magResult.dec));
|
|
} else {
|
|
jQuery('#selected_mag_declination').updateText('n/a');
|
|
}
|
|
|
|
let heading = null;
|
|
if (selected.true_heading != null && selected.track != null) {
|
|
heading = selected.true_heading;
|
|
} else if (magResult && selected.mag_heading != null && selected.track != null) {
|
|
heading = selected.mag_heading + magResult.dec;
|
|
}
|
|
if (heading != null && heading < 0)
|
|
heading += 360;
|
|
if (heading != null && heading > 360)
|
|
heading -= 360;
|
|
|
|
jQuery('#selected_mag_heading').updateText(format_track_brief(selected.mag_heading));
|
|
|
|
if (selected.wd != null && selected.ws != null) {
|
|
jQuery('#selected_wd').updateText(format_track_brief(selected.wd, true));
|
|
jQuery('#selected_ws').updateText(format_speed_long(selected.ws, DisplayUnits));
|
|
} else if (!globeIndex && magResult && selected.gs != null && selected.tas != null && selected.track != null && selected.mag_heading != null) {
|
|
|
|
const trk = (Math.PI / 180) * selected.track;
|
|
const hdg = (Math.PI / 180) * heading;
|
|
const tas = selected.tas;
|
|
const gs = selected.gs;
|
|
const ws = Math.round(Math.sqrt(Math.pow(tas - gs, 2) + 4 * tas * gs * Math.pow(Math.sin((hdg - trk) / 2), 2)));
|
|
let wd = trk + Math.atan2(tas * Math.sin(hdg - trk), tas * Math.cos(hdg - trk) - gs);
|
|
if (wd < 0) {
|
|
wd = wd + 2 * Math.PI;
|
|
}
|
|
if (wd > 2 * Math.PI) {
|
|
wd = wd - 2 * Math.PI;
|
|
}
|
|
wd = Math.round((180 / Math.PI) * wd);
|
|
jQuery('#selected_wd').updateText(format_track_brief(wd, true));
|
|
jQuery('#selected_ws').updateText(format_speed_long(ws, DisplayUnits));
|
|
} else {
|
|
jQuery('#selected_wd').updateText('n/a');
|
|
jQuery('#selected_ws').updateText('n/a');
|
|
}
|
|
|
|
|
|
if (!globeIndex && selected.true_heading == null && heading != null)
|
|
jQuery('#selected_true_heading').updateText(format_track_brief(heading));
|
|
else
|
|
jQuery('#selected_true_heading').updateText(format_track_brief(selected.true_heading));
|
|
|
|
|
|
let oat = null;
|
|
let tat = null;
|
|
|
|
if (selected.tat != null && selected.oat != null) {
|
|
oat = selected.oat;
|
|
tat = selected.tat;
|
|
} else if (!globeIndex && selected.mach != null && selected.tas != null && selected.mach > 0.395) {
|
|
oat = Math.pow((selected.tas / 661.47 / selected.mach), 2) * 288.15 - 273.15;
|
|
tat = -273.15 + (oat + 273.15) * (1 + 0.2 * selected.mach * selected.mach);
|
|
}
|
|
|
|
|
|
if (oat != null)
|
|
jQuery('#selected_temp').updateText(Math.round(tat) + ' / ' + Math.round(oat) + ' °C');
|
|
else
|
|
jQuery('#selected_temp').updateText('n/a');
|
|
|
|
jQuery('#selected_speed1').updateText(format_speed_long(selected.gs, DisplayUnits));
|
|
jQuery('#selected_speed2').updateText(format_speed_long(selected.gs, DisplayUnits));
|
|
jQuery('#selected_ias').updateText(format_speed_long(selected.ias, DisplayUnits));
|
|
jQuery('#selected_tas').updateText(format_speed_long(selected.tas, DisplayUnits));
|
|
jQuery('#selected_vert_rate').updateText(format_vert_rate_long(selected.vert_rate, DisplayUnits));
|
|
jQuery('#selected_baro_rate').updateText(format_vert_rate_long(selected.baro_rate, DisplayUnits));
|
|
jQuery('#selected_geom_rate').updateText(format_vert_rate_long(selected.geom_rate, DisplayUnits));
|
|
|
|
setSelectedIcao();
|
|
|
|
jQuery('#selected_pf_info').updateText((selected.pfRoute ? selected.pfRoute : "") );
|
|
//+" "+ (selected.pfFlightno ? selected.pfFlightno : "")
|
|
jQuery('#airframes_post_icao').attr('value',selected.icao);
|
|
jQuery('#selected_track1').updateText(format_track_brief(selected.track));
|
|
jQuery('#selected_track2').updateText(format_track_brief(selected.track));
|
|
|
|
if (selected.seen != null && selected.seen < 1000000) {
|
|
jQuery('#selected_seen').updateText(format_duration(selected.seen));
|
|
} else {
|
|
jQuery('#selected_seen').updateText('n/a');
|
|
}
|
|
if (selected.position_time != null) {
|
|
jQuery('#selected_pos_epoch').updateText(Math.round(selected.position_time));
|
|
} else {
|
|
jQuery('#selected_pos_epoch').updateText('n/a');
|
|
}
|
|
if (selected.seen_pos != null && selected.seen_pos < 1000000) {
|
|
jQuery('#selected_seen_pos').updateText(format_duration(selected.seen_pos));
|
|
} else {
|
|
jQuery('#selected_seen_pos').updateText('n/a');
|
|
}
|
|
|
|
jQuery('#selected_country').updateText(selected.icaorange.country.replace("special use", "special"));
|
|
if (ShowFlags && selected.icaorange.flag_image !== null) {
|
|
jQuery('#selected_flag').removeClass('hidden');
|
|
jQuery('#selected_flag img').attr('src', FlagPath + selected.icaorange.flag_image);
|
|
jQuery('#selected_flag img').attr('title', selected.icaorange.country);
|
|
} else {
|
|
jQuery('#selected_flag').addClass('hidden');
|
|
}
|
|
|
|
if (selected.position == null) {
|
|
jQuery('#selected_position').updateText('n/a');
|
|
} else {
|
|
|
|
if (selected.seen_pos > -1) {
|
|
jQuery('#selected_position').updateText(format_latlng(selected.position));
|
|
} else {
|
|
jQuery('#selected_position').updateText(format_latlng(selected.position));
|
|
}
|
|
}
|
|
if (selected.position && SitePosition) {
|
|
selected.sitedist = ol.sphere.getDistance(SitePosition, selected.position);
|
|
}
|
|
jQuery('#selected_source').updateText(format_data_source(selected.dataSource));
|
|
jQuery('#selected_category').updateText(selected.category ? selected.category : "n/a");
|
|
jQuery('#selected_category_label').updateText(get_category_label(selected.category));
|
|
jQuery('#selected_sitedist1').updateText(format_distance_long(selected.sitedist, DisplayUnits));
|
|
jQuery('#selected_sitedist2').updateText(format_distance_long(selected.sitedist, DisplayUnits));
|
|
jQuery('#selected_rssi1').updateText(selected.rssi != null ? selected.rssi.toFixed(1) : "n/a");
|
|
if (globeIndex && binCraft && !showTrace) {
|
|
jQuery('#selected_message_count').prev().updateText('Receivers:');
|
|
jQuery('#selected_message_count').prop('title', 'Number of receivers receiving this aircraft');
|
|
if (selected.receiverCount >= 5 && selected.dataSource != 'mlat') {
|
|
jQuery('#selected_message_count').updateText('> ' + selected.receiverCount);
|
|
} else {
|
|
jQuery('#selected_message_count').updateText(selected.receiverCount);
|
|
}
|
|
} else {
|
|
jQuery('#selected_message_count').prev().updateText('Messages:');
|
|
jQuery('#selected_message_count').prop('title', 'The total number of messages received from this aircraft');
|
|
jQuery('#selected_message_count').updateText(selected.messages);
|
|
}
|
|
jQuery('#selected_message_rate').updateText((selected.messageRate != null) ? (selected.messageRate.toFixed(1)) : "n/a");
|
|
jQuery('#selected_photo_link').html(getPhotoLink(selected));
|
|
|
|
jQuery('#selected_altitude_geom1').updateText(format_altitude_long(adjust_geom_alt(selected.alt_geom, selected.position), selected.geom_rate, DisplayUnits));
|
|
jQuery('#selected_altitude_geom2').updateText(format_altitude_long(adjust_geom_alt(selected.alt_geom, selected.position), selected.geom_rate, DisplayUnits));
|
|
jQuery('#selected_ias').updateText(format_speed_long(selected.ias, DisplayUnits));
|
|
jQuery('#selected_tas').updateText(format_speed_long(selected.tas, DisplayUnits));
|
|
if (selected.mach == null) {
|
|
jQuery('#selected_mach').updateText('n/a');
|
|
} else {
|
|
jQuery('#selected_mach').updateText(selected.mach.toFixed(3));
|
|
}
|
|
if (selected.roll == null) {
|
|
jQuery('#selected_roll').updateText('n/a');
|
|
} else {
|
|
jQuery('#selected_roll').updateText(selected.roll.toFixed(1));
|
|
}
|
|
if (selected.track_rate == null) {
|
|
jQuery('#selected_trackrate').updateText('n/a');
|
|
} else {
|
|
jQuery('#selected_trackrate').updateText(selected.track_rate.toFixed(2));
|
|
}
|
|
jQuery('#selected_geom_rate').updateText(format_vert_rate_long(selected.geom_rate, DisplayUnits));
|
|
if (selected.nav_qnh == null) {
|
|
jQuery('#selected_nav_qnh').updateText("n/a");
|
|
} else {
|
|
jQuery('#selected_nav_qnh').updateText(selected.nav_qnh.toFixed(1) + " hPa");
|
|
}
|
|
jQuery('#selected_nav_altitude').updateText(format_altitude_long(selected.nav_altitude, 0, DisplayUnits));
|
|
jQuery('#selected_nav_heading').updateText(format_track_brief(selected.nav_heading));
|
|
if (selected.nav_modes == null) {
|
|
jQuery('#selected_nav_modes').updateText("n/a");
|
|
} else {
|
|
jQuery('#selected_nav_modes').updateText(selected.nav_modes.join());
|
|
}
|
|
if (selected.nic_baro == null) {
|
|
jQuery('#selected_nic_baro').updateText("n/a");
|
|
} else {
|
|
if (selected.nic_baro == 1) {
|
|
jQuery('#selected_nic_baro').updateText("cross-checked");
|
|
} else {
|
|
jQuery('#selected_nic_baro').updateText("not cross-checked");
|
|
}
|
|
}
|
|
|
|
jQuery('#selected_nac_p').updateText(format_nac_p(selected.nac_p));
|
|
jQuery('#selected_nac_v').updateText(format_nac_v(selected.nac_v));
|
|
if (selected.rc == null) {
|
|
jQuery('#selected_rc').updateText("n/a");
|
|
} else if (selected.rc == 0) {
|
|
jQuery('#selected_rc').updateText("unknown");
|
|
} else {
|
|
jQuery('#selected_rc').updateText(format_distance_short(selected.rc, DisplayUnits));
|
|
}
|
|
|
|
if (selected.sil == null || selected.sil_type == null) {
|
|
jQuery('#selected_sil').updateText("n/a");
|
|
} else {
|
|
let sampleRate = "";
|
|
let silDesc = "";
|
|
if (selected.sil_type == "perhour") {
|
|
sampleRate = " per flight hour";
|
|
} else if (selected.sil_type == "persample") {
|
|
sampleRate = " per sample";
|
|
}
|
|
|
|
switch (selected.sil) {
|
|
case 0:
|
|
silDesc = "> 1e-3";
|
|
break;
|
|
case 1:
|
|
silDesc = "≤ 1e-3";
|
|
break;
|
|
case 2:
|
|
silDesc = "≤ 1e-5";
|
|
break;
|
|
case 3:
|
|
silDesc = "≤ 1e-7";
|
|
break;
|
|
default:
|
|
silDesc = "n/a";
|
|
sampleRate = "";
|
|
break;
|
|
}
|
|
jQuery('#selected_sil').html(silDesc + sampleRate);
|
|
}
|
|
|
|
if (selected.version == null) {
|
|
jQuery('#selected_version').updateText('none');
|
|
} else if (selected.version == 0) {
|
|
jQuery('#selected_version').updateText('v0 (DO-260)');
|
|
} else if (selected.version == 1) {
|
|
jQuery('#selected_version').updateText('v1 (DO-260A)');
|
|
} else if (selected.version == 2) {
|
|
jQuery('#selected_version').updateText('v2 (DO-260B)');
|
|
} else {
|
|
jQuery('#selected_version').updateText('v' + selected.version);
|
|
}
|
|
|
|
adjustInfoBlock();
|
|
}
|
|
|
|
let somethingHighlighted = false;
|
|
function refreshHighlighted() {
|
|
// this is following nearly identical logic, etc, as the refreshSelected function, but doing less junk for the highlighted pane
|
|
let highlighted = HighlightedPlane;
|
|
|
|
if (!highlighted) {
|
|
if (somethingHighlighted)
|
|
jQuery('#highlighted_infoblock').hide();
|
|
somethingHighlighted = false;
|
|
return;
|
|
}
|
|
somethingHighlighted = true;
|
|
|
|
highlighted.checkVisible();
|
|
|
|
jQuery('#highlighted_infoblock').show();
|
|
|
|
let infoBox = jQuery('#highlighted_infoblock');
|
|
|
|
let marker = highlighted.marker || highlighted.glMarker;
|
|
let geom;
|
|
let markerCoordinates;
|
|
if (!marker || !(geom = marker.getGeometry()) || !(markerCoordinates = geom.getCoordinates()) ) {
|
|
jQuery('#highlighted_infoblock').hide();
|
|
return;
|
|
}
|
|
let markerPosition = OLMap.getPixelFromCoordinate(markerCoordinates);
|
|
if (!markerPosition)
|
|
return;
|
|
|
|
let mapSize = OLMap.getSize();
|
|
if (markerPosition[0] + 200 < mapSize[0])
|
|
infoBox.css("left", markerPosition[0] + 20);
|
|
else
|
|
infoBox.css("left", markerPosition[0] - 200);
|
|
if (markerPosition[1] + 250 < mapSize[1])
|
|
infoBox.css("top", markerPosition[1] + 50);
|
|
else
|
|
infoBox.css("top", markerPosition[1] - 250);
|
|
|
|
jQuery('#highlighted_callsign').text(highlighted.name);
|
|
|
|
if (highlighted.icaoType !== null) {
|
|
jQuery('#highlighted_icaotype').text(highlighted.icaoType);
|
|
} else {
|
|
jQuery('#highlighted_icaotype').text("n/a");
|
|
}
|
|
|
|
jQuery('#highlighted_source').text(format_data_source(highlighted.getDataSource()));
|
|
|
|
if (highlighted.registration !== null) {
|
|
jQuery('#highlighted_registration').text(highlighted.registration);
|
|
} else {
|
|
jQuery('#highlighted_registration').text("n/a");
|
|
}
|
|
|
|
jQuery('#highlighted_speed').text(format_speed_long(highlighted.gs, DisplayUnits));
|
|
|
|
jQuery("#highlighted_altitude").text(format_altitude_long(highlighted.altitude, highlighted.vert_rate, DisplayUnits));
|
|
|
|
jQuery('#highlighted_pf_route').text((highlighted.pfRoute ? highlighted.pfRoute : highlighted.icao.toUpperCase()));
|
|
|
|
jQuery('#highlighted_rssi').text(highlighted.rssi != null ? highlighted.rssi.toFixed(1) + ' dBFS' : "n/a");
|
|
}
|
|
|
|
function removeHighlight() {
|
|
HighlightedPlane = null;
|
|
refreshHighlighted();
|
|
}
|
|
|
|
function refreshFeatures() {
|
|
for (let i in PlanesOrdered) {
|
|
PlanesOrdered[i].updateFeatures(true);
|
|
}
|
|
}
|
|
|
|
//
|
|
// Planes table begin
|
|
//
|
|
(function (global, jQuery, TAR) {
|
|
let planeMan = TAR.planeMan = TAR.planeMan || {};
|
|
|
|
function compareAlpha(xa,ya) {
|
|
if (xa === ya)
|
|
return 0;
|
|
if (xa < ya)
|
|
return -1;
|
|
return 1;
|
|
}
|
|
|
|
function compareBeta(xa, ya) {
|
|
if (xa === ya)
|
|
return 0;
|
|
if (sortAscending && xa < ya)
|
|
return -1;
|
|
if (!sortAscending && (xa.replace(/ /g, "").split("").reverse().join("") > ya.replace(/ /g, "").split("").reverse().join("")))
|
|
return -1;
|
|
return 1;
|
|
}
|
|
|
|
function compareNumeric(xf,yf) {
|
|
if (Math.abs(xf - yf) < 1e-9)
|
|
return 0;
|
|
|
|
return xf - yf;
|
|
}
|
|
|
|
const cols = planeMan.cols = {};
|
|
|
|
cols.icao = {
|
|
text: 'Hex ID',
|
|
sort: function () { sortBy('icao', compareAlpha, function(x) { return x.icao; }); },
|
|
value: function(plane) { return plane.icao; },
|
|
td: '<td class="icaoCodeColumn">',
|
|
};
|
|
cols.flag = {
|
|
text: 'Flag',
|
|
header: function() { return ""; },
|
|
sort: function () { sortBy('country', compareAlpha, function(x) { return x.icaorange.country; }); },
|
|
value: function(plane) { return (plane.icaorange.flag_image ? ('<img width="20" height="12" style="display: block;margin: auto;" src="' + FlagPath + plane.icaorange.flag_image + '" title="' + plane.icaorange.country + '"></img>') : ''); },
|
|
hStyle: 'style="width: 20px; padding: 3px;"',
|
|
html: true,
|
|
};
|
|
cols.flight = {
|
|
sort: function () { sortBy('flight', compareAlpha, function(x) { return x.flight }); },
|
|
value: function(plane) {
|
|
if (flightawareLinks)
|
|
return getFlightAwareModeSLink(plane.icao, plane.flight, plane.name);
|
|
return (plane.flight || '');
|
|
},
|
|
html: flightawareLinks,
|
|
text: 'Callsign' };
|
|
cols.registration = {
|
|
sort: function () { sortBy('registration', compareAlpha, function(x) { return x.registration; }); },
|
|
value: function(plane) { return (flightawareLinks ? getFlightAwareIdentLink(plane.registration, plane.registration) : (plane.registration ? plane.registration : "")); },
|
|
html: flightawareLinks,
|
|
text: 'Registration' };
|
|
cols.aircraft_type = {
|
|
sort: function () { sortBy('type', compareAlpha, function(x) { return x.icaoType; }); },
|
|
value: function(plane) { return (plane.icaoType != null ? plane.icaoType : ""); },
|
|
text: 'Type' };
|
|
cols.squawk = {
|
|
text: 'Squawk',
|
|
sort: function () { sortBy('squawk', compareAlpha, function(x) { return x.squawk; }); },
|
|
value: function(plane) { return (plane.squawk != null ? plane.squawk : ""); },
|
|
align: 'right' };
|
|
cols.altitude = {
|
|
text: 'Altitude',
|
|
sort: function () { sortBy('altitude',compareNumeric, function(x) { return (x.altitude == "ground" ? -100000 : x.altitude); }); },
|
|
value: function(plane) { return format_altitude_brief(plane.altitude, plane.vert_rate, DisplayUnits); },
|
|
align: 'right',
|
|
header: function () { return 'Alt.' + NBSP + '(' + get_unit_label("altitude", DisplayUnits) + ')';},
|
|
};
|
|
cols.speed = {
|
|
text: pTracks ? 'Max. Speed' : 'Speed',
|
|
sort: function () { sortBy('speed', compareNumeric, function(x) { return x.speed; }); },
|
|
value: function(plane) { return format_speed_brief(plane.speed, DisplayUnits); },
|
|
align: 'right',
|
|
header: function () { return (pTracks ? 'Max. ' : '') + 'Spd.' + NBSP + '(' + get_unit_label("speed", DisplayUnits) + ')';},
|
|
};
|
|
cols.vert_rate = {
|
|
text: 'Vertical Rate',
|
|
sort: function () { sortBy('vert_rate', compareNumeric, function(x) { return x.vert_rate; }); },
|
|
value: function(plane) { return format_vert_rate_brief(plane.vert_rate, DisplayUnits); },
|
|
align: 'right',
|
|
header: function () { return 'V. Rate(' + get_unit_label("verticalRate", DisplayUnits) + ')';},
|
|
};
|
|
cols.distance = {
|
|
text: pTracks ? 'Max. Distance' : 'Distance',
|
|
sort: function () { sortBy('sitedist',compareNumeric, function(x) { return x.sitedist; }); },
|
|
value: function(plane) { return format_distance_brief(plane.sitedist, DisplayUnits); },
|
|
align: 'right',
|
|
header: function () { return (pTracks ? 'Max. ' : '') + 'Dist.' + NBSP + '(' + get_unit_label("distance", DisplayUnits) + ')';},
|
|
};
|
|
cols.track = {
|
|
text: 'Track',
|
|
sort: function () { sortBy('track', compareNumeric, function(x) { return x.track; }); },
|
|
value: function(plane) { return format_track_brief(plane.track); },
|
|
align: 'right' };
|
|
cols.msgs = {
|
|
text: 'Messages',
|
|
sort: function () { sortBy('msgs', compareNumeric, function(x) { return x.messages; }); },
|
|
value: function(plane) { return plane.messages; },
|
|
align: 'right' };
|
|
cols.seen = {
|
|
text: 'Seen',
|
|
sort: function () { sortBy('seen', compareNumeric, function(x) { return x.seen; }); },
|
|
value: function(plane) { return plane.seen.toFixed(0); },
|
|
align: 'right' };
|
|
cols.rssi = {
|
|
text: 'RSSI',
|
|
sort: function () { sortBy('rssi', compareNumeric, function(x) { return x.rssi; }); },
|
|
value: function(plane) { return (plane.rssi != null ? plane.rssi.toFixed(1) : ""); },
|
|
align: 'right' };
|
|
cols.lat = {
|
|
text: 'Latitude',
|
|
sort: function () { sortBy('lat', compareNumeric, function(x) { return (x.position !== null ? x.position[1] : null); }); },
|
|
value: function(plane) { return (plane.position != null ? plane.position[1].toFixed(4) : ""); },
|
|
align: 'right' };
|
|
cols.lon = {
|
|
text: 'Longitude',
|
|
sort: function () { sortBy('lon', compareNumeric, function(x) { return (x.position !== null ? x.position[0] : null); }); },
|
|
value: function(plane) { return (plane.position != null ? plane.position[0].toFixed(4) : ""); },
|
|
align: 'right' };
|
|
cols.data_source = {
|
|
text: 'Source',
|
|
sort: function () { sortBy('data_source', compareNumeric, function(x) { return x.getDataSourceNumber(); } ); },
|
|
value: function(plane) { return format_data_source(plane.getDataSource()); },
|
|
align: 'right' };
|
|
cols.military = {
|
|
text: 'Mil.',
|
|
sort: function () { sortBy('military', compareAlpha, function(x) { return (x.military ? 'yes' : 'no'); } ); },
|
|
value: function(plane) { return (plane.military ? 'yes' : 'no'); },
|
|
align: 'right' };
|
|
cols.wd = {
|
|
text: 'Wind D.',
|
|
sort: function () { sortBy('wd', compareNumeric, function(x) { return plane.wd; }); },
|
|
value: function(plane) { return plane.wd != null ? (plane.wd + '°') : ''; },
|
|
align: 'right' };
|
|
cols.ws = {
|
|
text: 'Wind S.',
|
|
sort: function () { sortBy('ws', compareNumeric, function(x) { return x.ws; }); },
|
|
value: function(plane) { return format_speed_brief(plane.ws, DisplayUnits); },
|
|
align: 'right',
|
|
header: function () { return 'Wind' + NBSP + '(' + get_unit_label("speed", DisplayUnits) + ')'; },
|
|
};
|
|
|
|
const colsEntries = Object.entries(cols);
|
|
for (let i in colsEntries) {
|
|
let key = colsEntries[i][0];
|
|
let value = colsEntries[i][1];
|
|
value.id = key;
|
|
value.text = value.text ? value.text : "";
|
|
value.header = value.header ? value.header : function() { return value.text; };
|
|
value.hStyle = value.hStyle ? value.hStyle : "";
|
|
if (!value.td)
|
|
value.td = value.align ? ('<td style="text-align: ' + value.align + '">') : '<td>';
|
|
}
|
|
|
|
let columns = createOrderedColumns();
|
|
let activeCols = null;
|
|
|
|
let initializing = true;
|
|
|
|
let planeRowTemplate = null;
|
|
planeMan.lastRenderExtent = null;
|
|
let htmlTable = null;
|
|
let tbody = null;
|
|
|
|
planeMan.init = function () {
|
|
// initialize columns
|
|
htmlTable = document.getElementById('planesTable');
|
|
for (let i in columns) {
|
|
let col = columns[i];
|
|
col.visible = true;
|
|
col.toggleKey = 'column_' + col.id;
|
|
|
|
if (HideCols.includes('#' + col.id)) {
|
|
planeMan.setColumnVis(col.id, false);
|
|
}
|
|
}
|
|
|
|
createColumnToggles();
|
|
|
|
if (!ShowFlags) {
|
|
planeMan.setColumnVis('flag', false);
|
|
}
|
|
|
|
planeMan.redraw();
|
|
initializing = false;
|
|
}
|
|
|
|
planeMan.redraw = function () {
|
|
activeCols = [];
|
|
for (let i in columns) {
|
|
let col = columns[i];
|
|
if (col.visible || !mapIsVisible) {
|
|
activeCols.push(col);
|
|
}
|
|
}
|
|
for (let i = 0; i < PlanesOrdered.length; ++i) {
|
|
PlanesOrdered[i].destroyTR();
|
|
}
|
|
let table = '';
|
|
table += '<thead class="aircraft_table_header">';
|
|
table += ' <tr>';
|
|
for (let i in activeCols) {
|
|
let col = activeCols[i];
|
|
table += '<td id="' + col.id + '" onclick="TAR.planeMan.cols.' + col.id + '.sort();"' + col.hStyle + '>'+ col.header() +'</td>';
|
|
}
|
|
table += ' </tr>';
|
|
table += '</thead>';
|
|
table += '<tbody>';
|
|
table += '</tbody>';
|
|
htmlTable.innerHTML = table;
|
|
tbody = htmlTable.tBodies[0];
|
|
|
|
planeRowTemplate = document.createElement('tr');
|
|
let template = ''
|
|
for (let i in activeCols) {
|
|
let col = activeCols[i];
|
|
template += col.td;
|
|
template += '</td>';
|
|
}
|
|
planeRowTemplate.innerHTML = template;
|
|
|
|
planeMan.refresh();
|
|
}
|
|
|
|
planeMan.setColumnVis = function (col, visible) {
|
|
cols[col].visible = visible;
|
|
|
|
if (!initializing)
|
|
planeMan.redraw();
|
|
}
|
|
|
|
// Refreshes the larger table of all the planes
|
|
planeMan.refresh = function () {
|
|
if (initializing)
|
|
return;
|
|
|
|
const ctime = false; // gets enabled for debugging table refresh speed
|
|
// globeTableLimit = 1000; for testing performance
|
|
|
|
ctime && console.time("planeMan.refresh()");
|
|
|
|
|
|
if (mapIsVisible || planeMan.lastRenderExtent === null) {
|
|
const size = [OLMap.getSize()[0] + 45, OLMap.getSize()[1] + 45];
|
|
planeMan.lastRenderExtent = myExtent(OLMap.getView().calculateExtent(size));
|
|
}
|
|
|
|
TrackedAircraft = 0;
|
|
TrackedAircraftPositions = 0;
|
|
TrackedHistorySize = 0;
|
|
|
|
ctime && console.time("inView");
|
|
let pList = []; // list of planes that might go in the table and need sorting
|
|
for (let i = 0; i < PlanesOrdered.length; ++i) {
|
|
const plane = PlanesOrdered[i];
|
|
|
|
plane.visible = plane.checkVisible() && !plane.isFiltered()
|
|
|
|
|
|
if ((globeIndex || replay) && SelectedAllPlanes && noVanish) {
|
|
// pretend planes never go off the map for trails:
|
|
plane.inView = true;
|
|
} else {
|
|
plane.inView = inView(plane.position, planeMan.lastRenderExtent);
|
|
}
|
|
|
|
TrackedHistorySize += plane.history_size;
|
|
|
|
if (tableInView) {
|
|
if (plane.visible)
|
|
TrackedAircraft++;
|
|
if ((plane.inView && plane.visible) || plane.selected) {
|
|
pList.push(plane);
|
|
TrackedAircraftPositions++;
|
|
}
|
|
} else {
|
|
if (plane.visible) {
|
|
TrackedAircraft++;
|
|
pList.push(plane);
|
|
if (plane.position != null)
|
|
TrackedAircraftPositions++;
|
|
}
|
|
}
|
|
}
|
|
ctime && console.timeEnd("inView");
|
|
|
|
ctime && console.time("resortTable");
|
|
resortTable(pList);
|
|
ctime && console.timeEnd("resortTable");
|
|
|
|
const sidebarVisible = toggles['sidebar_visible'].state;
|
|
|
|
let inTable = []; // list of planes that will actually be displayed in the table
|
|
|
|
ctime && console.time("modTRs");
|
|
for (let i in pList) {
|
|
const plane = pList[i];
|
|
|
|
if (!sidebarVisible || (inTable.length > globeTableLimit && mapIsVisible && globeIndex)) {
|
|
break;
|
|
}
|
|
inTable.push(plane);
|
|
|
|
if (plane.tr == null) {
|
|
plane.makeTR(planeRowTemplate.cloneNode(true));
|
|
plane.tr.id = plane.icao;
|
|
plane.refreshTR = 0;
|
|
}
|
|
|
|
if (now - plane.refreshTR > 5 || plane.selected != plane.selectCache) {
|
|
plane.refreshTR = now;
|
|
let colors = tableColors.unselected;
|
|
let bgColor = "#F8F8F8"
|
|
|
|
plane.selectCache = plane.selected;
|
|
if (plane.selected)
|
|
colors = tableColors.selected;
|
|
|
|
if (plane.dataSource && plane.dataSource in colors)
|
|
bgColor = colors[plane.dataSource];
|
|
|
|
if (plane.squawk in tableColors.special) {
|
|
bgColor = tableColors.special[plane.squawk];
|
|
plane.bgColorCache = bgColor;
|
|
plane.tr.style = "background-color: " + bgColor + "; color: black;";
|
|
} else if (plane.bgColorCache != bgColor) {
|
|
plane.bgColorCache = bgColor;
|
|
plane.tr.style = "background-color: " + bgColor + ";";
|
|
}
|
|
|
|
for (let cell in activeCols) {
|
|
let col = activeCols[cell];
|
|
if (!col.value)
|
|
continue;
|
|
let newValue = col.value(plane);
|
|
if (newValue != plane.trCache[cell]) {
|
|
plane.trCache[cell] = newValue;
|
|
if (col.html) {
|
|
plane.tr.cells[cell].innerHTML = newValue;
|
|
} else {
|
|
plane.tr.cells[cell].textContent = newValue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ctime && console.timeEnd("modTRs");
|
|
|
|
global.refreshPageTitle();
|
|
jQuery('#dump1090_total_history').updateText(TrackedHistorySize);
|
|
jQuery('#dump1090_message_rate').updateText(MessageRate === null ? 'n/a' : MessageRate.toFixed(1));
|
|
jQuery('#dump1090_total_ac').updateText(globeIndex ? globeTrackedAircraft : TrackedAircraft);
|
|
jQuery('#dump1090_total_ac_positions').updateText(TrackedAircraftPositions);
|
|
|
|
|
|
|
|
ctime && console.time("DOM1");
|
|
|
|
let newBody = document.createElement('tbody');
|
|
for (let i in inTable) {
|
|
const plane = inTable[i];
|
|
newBody.appendChild(plane.tr);
|
|
}
|
|
|
|
ctime && console.timeEnd("DOM1");
|
|
ctime && console.time("DOM2");
|
|
|
|
htmlTable.replaceChild(newBody, tbody);
|
|
tbody = newBody;
|
|
|
|
ctime && console.timeEnd("DOM2");
|
|
|
|
ctime && console.timeEnd("planeMan.refresh()");
|
|
}
|
|
|
|
//
|
|
// ---- table sorting begin ----
|
|
//
|
|
|
|
let sortId = '';
|
|
let sortCompare = null;
|
|
let sortExtract = null;
|
|
let sortAscending = true;
|
|
|
|
function sortFunction(x,y) {
|
|
const xv = x._sort_value;
|
|
const yv = y._sort_value;
|
|
|
|
// always sort missing values at the end, regardless of
|
|
// ascending/descending sort
|
|
if (xv == null && yv == null) return x._sort_pos - y._sort_pos;
|
|
if (xv == null) return 1;
|
|
if (yv == null) return -1;
|
|
|
|
const c = sortAscending ? sortCompare(xv,yv) : sortCompare(yv,xv);
|
|
if (c !== 0) return c;
|
|
|
|
return x._sort_pos - y._sort_pos;
|
|
}
|
|
|
|
function resortTable(pList) {
|
|
if (!sortExtract)
|
|
return;
|
|
if (globeIndex) {
|
|
// don't presort for globeIndex
|
|
}
|
|
// presort by dataSource
|
|
else if (sortId == "sitedist") {
|
|
for (let i = 0; i < pList.length; ++i) {
|
|
pList[i]._sort_pos = i;
|
|
}
|
|
pList.sort(function(x,y) {
|
|
const a = x.getDataSourceNumber();
|
|
const b = y.getDataSourceNumber();
|
|
if (a == b)
|
|
return (x._sort_pos - y._sort_pos);
|
|
|
|
return (a-b);
|
|
});
|
|
}
|
|
// or distance
|
|
else if (sortId == "data_source") {
|
|
pList.sort(function(x,y) {
|
|
return (x.sitedist - y.sitedist);
|
|
});
|
|
}
|
|
// or longitude
|
|
else {
|
|
pList.sort(function(x,y) {
|
|
return (x.position ? x.position[0] : 500) - (y.position ? y.position[0] : 500);
|
|
});
|
|
}
|
|
|
|
// number the existing rows so we can do a stable sort
|
|
// regardless of whether sort() is stable or not.
|
|
// Also extract the sort comparison value.
|
|
if (globeIndex) {
|
|
for (let i = 0; i < pList.length; ++i) {
|
|
pList[i]._sort_pos = pList[i].numHex;
|
|
pList[i]._sort_value = sortExtract(pList[i]);
|
|
}
|
|
} else {
|
|
for (let i = 0; i < pList.length; ++i) {
|
|
pList[i]._sort_pos = i;
|
|
pList[i]._sort_value = sortExtract(pList[i]);
|
|
}
|
|
}
|
|
|
|
pList.sort(sortFunction);
|
|
|
|
// In multiSelect put selected planes on top, do a stable sort!
|
|
if (multiSelect) {
|
|
for (let i = 0; i < pList.length; ++i) {
|
|
pList[i]._sort_pos = i;
|
|
}
|
|
pList.sort(function(x,y) {
|
|
if (x.selected && y.selected) {
|
|
return (x._sort_pos - y._sort_pos);
|
|
}
|
|
if (x.selected)
|
|
return -1;
|
|
if (y.selected)
|
|
return 1;
|
|
|
|
return (x._sort_pos - y._sort_pos);
|
|
});
|
|
}
|
|
}
|
|
|
|
function sortBy(id, sc, se) {
|
|
if (id != 'data_source' && grouptype_checkbox) {
|
|
jQuery('#grouptype_checkbox').removeClass('settingsCheckboxChecked');
|
|
grouptype_checkbox = false;
|
|
} else if (id == 'data_source' && !grouptype_checkbox) {
|
|
jQuery('#grouptype_checkbox').addClass('settingsCheckboxChecked');
|
|
grouptype_checkbox = true;
|
|
}
|
|
|
|
if (id === sortId) {
|
|
sortAscending = !sortAscending;
|
|
PlanesOrdered.reverse(); // this correctly flips the order of rows that compare equal
|
|
} else {
|
|
sortAscending = true;
|
|
}
|
|
|
|
sortId = id;
|
|
sortCompare = sc;
|
|
sortExtract = se;
|
|
|
|
planeMan.refresh();
|
|
}
|
|
|
|
//
|
|
// ---- table sorting end ----
|
|
//
|
|
|
|
function createColumnToggles() {
|
|
const prefix = 'dd_';
|
|
const sortableColumns = jQuery('#sortableColumns').sortable({
|
|
update: function (event, ui) {
|
|
const order = [];
|
|
jQuery('#sortableColumns li').each(function (e) {
|
|
order.push(jQuery(this).attr('id').replace(prefix, ''));
|
|
});
|
|
|
|
loStore['columnOrder'] = JSON.stringify(order);
|
|
columns = createOrderedColumns();
|
|
|
|
planeMan.redraw();
|
|
}
|
|
});
|
|
|
|
for (let col of columns) {
|
|
sortableColumns.append(`<li class="ui-state-default" id="${prefix + col.id}"></li>`);
|
|
|
|
new Toggle({
|
|
key: col.toggleKey,
|
|
display: col.text,
|
|
container: jQuery(`#${prefix + col.id}`),
|
|
init: col.visible,
|
|
setState: function (state) {
|
|
planeMan.setColumnVis(col.id, state);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function createOrderedColumns() {
|
|
const order = loStore['columnOrder'];
|
|
if (order !== undefined) {
|
|
const columns = [];
|
|
for (let col of JSON.parse(order)) {
|
|
const column = cols[col];
|
|
if (column !== undefined) {
|
|
columns.push(column);
|
|
}
|
|
}
|
|
if (columns.length > 0) {
|
|
return columns;
|
|
}
|
|
}
|
|
return Object.values(cols);
|
|
}
|
|
|
|
return TAR;
|
|
}(window, jQuery, TAR || {}));
|
|
//
|
|
// Planes table end
|
|
//
|
|
|
|
function deselect(plane) {
|
|
if (!plane || !plane.selected)
|
|
return;
|
|
plane.selected = false;
|
|
const index = SelPlanes.indexOf(plane);
|
|
if (index > -1)
|
|
SelPlanes.splice(index, 1);
|
|
if (plane == SelectedPlane) {
|
|
sp = SelectedPlane = null;
|
|
refreshSelected();
|
|
}
|
|
|
|
plane.updateTick('redraw');
|
|
updateAddressBar();
|
|
}
|
|
let scount = 0;
|
|
function select(plane, options) {
|
|
if (!plane)
|
|
return;
|
|
options = options || {};
|
|
//console.log("select()", plane.icao, options);
|
|
plane.selected = true;
|
|
if (!SelPlanes.includes(plane))
|
|
SelPlanes.push(plane);
|
|
|
|
sp = SelectedPlane = plane;
|
|
updateAddressBar();
|
|
refreshSelected();
|
|
plane.updateTick('redraw');
|
|
|
|
if (options.follow) {
|
|
toggleFollow(true);
|
|
if (!options.zoom)
|
|
options.zoom = 'follow';
|
|
} else {
|
|
toggleFollow(false);
|
|
}
|
|
}
|
|
|
|
function selectPlaneByHex(hex, options) {
|
|
active();
|
|
console.log("SELECTING", hex, options);
|
|
options = options || {};
|
|
//console.log("select: " + hex);
|
|
// If SelectedPlane has something in it, clear out the selected
|
|
if (SelectedAllPlanes) {
|
|
deselectAllPlanes();
|
|
}
|
|
// already selected plane
|
|
let oldPlane = SelectedPlane;
|
|
// plane to be selected
|
|
let newPlane = Planes[hex];
|
|
|
|
if (!options.noFetch && globeIndex && hex)
|
|
newPlane = getTrace(newPlane, hex, options);
|
|
|
|
// If we are clicking the same plane, we are deselecting it unless noDeselect is specified
|
|
if (oldPlane == newPlane && (options.noDeselect || showTrace)) {
|
|
oldPlane = null;
|
|
} else {
|
|
if (multiSelect) {
|
|
// multiSelect deselect
|
|
if (newPlane && newPlane.selected && !onlySelected) {
|
|
deselect(newPlane);
|
|
newPlane = null;
|
|
hex = null;
|
|
}
|
|
} else if (oldPlane) {
|
|
// normal deselect
|
|
if (oldPlane != newPlane) {
|
|
deselect(oldPlane);
|
|
oldPlane = null;
|
|
}
|
|
if (oldPlane == newPlane) {
|
|
deselect(newPlane);
|
|
oldPlane = null;
|
|
newPlane = null;
|
|
hex = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Assign the new selected
|
|
select(newPlane, options);
|
|
|
|
if (!newPlane) {
|
|
toggleFollow(false);
|
|
}
|
|
|
|
if (options.zoom == 'follow') {
|
|
//if (OLMap.getView().getZoom() < 8)
|
|
// OLMap.getView().setZoom(8);
|
|
} else if (options.zoom) {
|
|
OLMap.getView().setZoom(options.zoom);
|
|
}
|
|
|
|
pTracks || TAR.planeMan.refresh();
|
|
|
|
return newPlane !== undefined;
|
|
}
|
|
|
|
// loop through the planes and mark them as selected to show the paths for all planes
|
|
function selectAllPlanes() {
|
|
HighlightedPlane = null;
|
|
// if all planes are already selected, deselect them all
|
|
if (SelectedAllPlanes) {
|
|
deselectAllPlanes();
|
|
return;
|
|
}
|
|
buttonActive('#T', true);
|
|
// If SelectedPlane has something in it, clear out the selected
|
|
if (SelectedPlane)
|
|
deselect(SelectedPlane);
|
|
|
|
toggleIsolation("off");
|
|
|
|
SelectedAllPlanes = true;
|
|
|
|
if (globeIndex) {
|
|
for (let i in PlanesOrdered) {
|
|
let plane = PlanesOrdered[i];
|
|
if (plane.visible && plane.inView) {
|
|
plane.processTrace();
|
|
}
|
|
}
|
|
}
|
|
refreshFeatures();
|
|
|
|
refreshSelected();
|
|
refreshHighlighted();
|
|
pTracks || TAR.planeMan.refresh();
|
|
}
|
|
|
|
// deselect all the planes
|
|
function deselectAllPlanes(keepMain) {
|
|
if (showTrace && !keepMain)
|
|
return;
|
|
if (!multiSelect && SelectedPlane)
|
|
toggleIsolation("off");
|
|
|
|
clearTimeout(getTraceTimeout);
|
|
|
|
if (SelectedAllPlanes) {
|
|
buttonActive('#T', false);
|
|
jQuery('#selectall_checkbox').removeClass('settingsCheckboxChecked');
|
|
SelectedAllPlanes = false;
|
|
refreshFilter();
|
|
return;
|
|
}
|
|
|
|
let bounce = [];
|
|
for (let i in SelPlanes) {
|
|
const plane = SelPlanes[i];
|
|
if (keepMain && plane == SelectedPlane)
|
|
continue;
|
|
bounce.push(plane);
|
|
}
|
|
for (let i in bounce) {
|
|
deselect(bounce[i]);
|
|
}
|
|
refreshFilter();
|
|
updateAddressBar();
|
|
}
|
|
|
|
function toggleFollow(override) {
|
|
if (override == true)
|
|
FollowSelected = true;
|
|
else if (override == false)
|
|
FollowSelected = false;
|
|
else
|
|
FollowSelected = !FollowSelected;
|
|
|
|
traceOpts.follow = FollowSelected;
|
|
|
|
if (FollowSelected) {
|
|
if (!SelectedPlane || !SelectedPlane.position)
|
|
FollowSelected = false;
|
|
}
|
|
if (FollowSelected) {
|
|
//if (override == undefined && OLMap.getView().getZoom() < 8)
|
|
// OLMap.getView().setZoom(8);
|
|
SelectedPlane.setProjection('follow');
|
|
}
|
|
buttonActive('#F', FollowSelected);
|
|
}
|
|
|
|
function resetMap() {
|
|
if (SitePosition) {
|
|
CenterLon = SiteLon;
|
|
CenterLat = SiteLat;
|
|
} else {
|
|
CenterLon = DefaultCenterLon;
|
|
CenterLat = DefaultCenterLat;
|
|
}
|
|
// Reset loStore values and map settings
|
|
loStore['CenterLat'] = CenterLat
|
|
loStore['CenterLon'] = CenterLon
|
|
//loStore['zoomLvl'] = zoomLvl = DefaultZoomLvl;
|
|
|
|
// Set and refresh
|
|
//OLMap.getView().setZoom(zoomLvl);
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([CenterLon, CenterLat]));
|
|
OLMap.getView().setRotation(mapOrientation);
|
|
|
|
//selectPlaneByHex(null,false);
|
|
jQuery("#update_error").css('display','none');
|
|
geoFindMe();
|
|
}
|
|
|
|
function updateMapSize() {
|
|
if (OLMap)
|
|
OLMap.updateSize();
|
|
}
|
|
|
|
function expandSidebar(e) {
|
|
e.preventDefault();
|
|
jQuery("#map_container").hide()
|
|
mapIsVisible = false;
|
|
jQuery("#toggle_sidebar_control").hide();
|
|
jQuery("#splitter").hide();
|
|
jQuery("#shrink_sidebar_button").show();
|
|
jQuery("#sidebar_container").width("100%");
|
|
TAR.planeMan.redraw();
|
|
updateMapSize();
|
|
adjustInfoBlock();
|
|
}
|
|
|
|
function showMap() {
|
|
jQuery('#sidebar_container').width(loStore['sidebar_width']).css('margin-left', '0');
|
|
jQuery("#map_container").show()
|
|
mapIsVisible = true;
|
|
jQuery("#toggle_sidebar_control").show();
|
|
jQuery("#splitter").show();
|
|
jQuery("#shrink_sidebar_button").hide();
|
|
TAR.planeMan.redraw();
|
|
updateMapSize();
|
|
}
|
|
|
|
|
|
let selectedPhotoCache = null;
|
|
|
|
function setPhotoHtml(source) {
|
|
if (selectedPhotoCache == source)
|
|
return;
|
|
//console.log(source + ' ' + selectedPhotoCache);
|
|
selectedPhotoCache = source;
|
|
jQuery('#selected_photo').html(source);
|
|
}
|
|
|
|
function adjustInfoBlock() {
|
|
if (wideInfoBlock ) {
|
|
infoBlockWidth = baseInfoBlockWidth + 40;
|
|
} else {
|
|
infoBlockWidth = baseInfoBlockWidth;
|
|
}
|
|
jQuery('#selected_infoblock').css("width", infoBlockWidth * globalScale + 'px');
|
|
|
|
jQuery('.ol-scale-line').css('left', (infoBlockWidth * globalScale + 8) + 'px');
|
|
jQuery('#replayBar').css('left', (infoBlockWidth * globalScale + 8) + 'px');
|
|
|
|
if (SelectedPlane && toggles['enableInfoblock'].state) {
|
|
|
|
if (!mapIsVisible)
|
|
jQuery("#sidebar_container").css('margin-left', '140pt');
|
|
//jQuery('#sidebar_canvas').css('margin-bottom', jQuery('#selected_infoblock').height() + 'px');
|
|
//
|
|
if (mapIsVisible && document.getElementById('map_canvas').clientWidth < parseFloat(jQuery('#selected_infoblock').css('width')) * 3) {
|
|
jQuery('#selected_infoblock').css('height', '290px');
|
|
jQuery('#selected_typedesc').parent().parent().hide();
|
|
jQuery('#credits').css('bottom', '295px');
|
|
jQuery('#credits').css('left', '5px');
|
|
} else {
|
|
jQuery('#selected_infoblock').css('height', '100%');
|
|
jQuery('#credits').css('bottom', '');
|
|
jQuery('#credits').css('left', '');
|
|
}
|
|
|
|
jQuery('#selected_infoblock').show();
|
|
} else {
|
|
if (!mapIsVisible)
|
|
jQuery("#sidebar_container").css('margin-left', '0');
|
|
//jQuery('#sidebar_canvas').css('margin-bottom', 0);
|
|
|
|
jQuery('.ol-scale-line').css('left', '8px');
|
|
jQuery('#replayBar').css('left', '0px');
|
|
jQuery('#credits').css('bottom', '');
|
|
jQuery('#credits').css('left', '');
|
|
|
|
jQuery('#selected_infoblock').hide();
|
|
}
|
|
|
|
let photoWidth = document.getElementById('photo_container').clientWidth;
|
|
let refWidth = infoBlockWidth * globalScale - 29;
|
|
if (Math.abs(photoWidth / refWidth - 1) > 0.05)
|
|
photoWidth = refWidth;
|
|
|
|
jQuery('#airplanePhoto').css("width", photoWidth + 'px');
|
|
jQuery('#selected_photo').css("width", photoWidth + 'px');
|
|
|
|
if (showPictures) {
|
|
if (planespottersAPI || planespottingAPI) {
|
|
jQuery('#photo_container').css('height', photoWidth * 0.883 + 'px');
|
|
} else {
|
|
jQuery('#photo_container').css('height', '40px');
|
|
}
|
|
}
|
|
}
|
|
|
|
function initializeUnitsSelector() {
|
|
// Get display unit preferences from local storage
|
|
if (!loStore.getItem('displayUnits')) {
|
|
loStore['displayUnits'] = 'nautical';
|
|
}
|
|
|
|
DisplayUnits = loStore['displayUnits'];
|
|
|
|
// Initialize drop-down
|
|
jQuery('#units_selector')
|
|
.val(DisplayUnits)
|
|
.on('change', onDisplayUnitsChanged);
|
|
|
|
jQuery(".altitudeUnit").text(get_unit_label("altitude", DisplayUnits));
|
|
jQuery(".speedUnit").text(get_unit_label("speed", DisplayUnits));
|
|
jQuery(".distanceUnit").text(get_unit_label("distance", DisplayUnits));
|
|
jQuery(".verticalRateUnit").text(get_unit_label("verticalRate", DisplayUnits));
|
|
}
|
|
|
|
function onDisplayUnitsChanged(e) {
|
|
loStore['displayUnits'] = DisplayUnits = e.target.value;
|
|
|
|
TAR.altitudeChart.render();
|
|
|
|
// Update filters
|
|
updateAltFilter();
|
|
|
|
// Refresh data
|
|
refreshFilter();
|
|
|
|
// Draw range rings
|
|
if (siteCircleLayer.getVisible()) {
|
|
drawSiteCircle();
|
|
}
|
|
|
|
// Reset map scale line units
|
|
OLMap.getControls().forEach(function(control) {
|
|
if (control instanceof ol.control.ScaleLine) {
|
|
control.setUnits(DisplayUnits);
|
|
}
|
|
});
|
|
|
|
jQuery(".altitudeUnit").text(get_unit_label("altitude", DisplayUnits));
|
|
jQuery(".speedUnit").text(get_unit_label("speed", DisplayUnits));
|
|
jQuery(".distanceUnit").text(get_unit_label("distance", DisplayUnits));
|
|
jQuery(".verticalRateUnit").text(get_unit_label("verticalRate", DisplayUnits));
|
|
TAR.planeMan.redraw();
|
|
|
|
remakeTrails();
|
|
refreshSelected();
|
|
}
|
|
|
|
function onFilterByAltitude(e) {
|
|
e.preventDefault();
|
|
jQuery("#altitude_filter_min").blur();
|
|
jQuery("#altitude_filter_max").blur();
|
|
|
|
updateAltFilter();
|
|
refreshFilter();
|
|
}
|
|
|
|
function refreshFilter() {
|
|
if (filterTracks)
|
|
remakeTrails();
|
|
|
|
TAR.planeMan.refresh();
|
|
refreshSelected();
|
|
refreshHighlighted();
|
|
mapRefresh(true);
|
|
|
|
drawHeatmap();
|
|
if (toggles.shareFilters && toggles.shareFilters.state) {
|
|
updateAddressBar();
|
|
}
|
|
}
|
|
|
|
function filterGroundVehicles(switchFilter) {
|
|
if (typeof loStore['groundVehicleFilter'] === 'undefined') {
|
|
loStore['groundVehicleFilter'] = 'not_filtered';
|
|
}
|
|
let groundFilter = loStore['groundVehicleFilter'];
|
|
if (switchFilter === true) {
|
|
groundFilter = (groundFilter === 'not_filtered') ? 'filtered' : 'not_filtered';
|
|
}
|
|
if (groundFilter === 'not_filtered') {
|
|
jQuery('#groundvehicle_filter').addClass('settingsCheckboxChecked');
|
|
} else {
|
|
jQuery('#groundvehicle_filter').removeClass('settingsCheckboxChecked');
|
|
}
|
|
loStore['groundVehicleFilter'] = groundFilter;
|
|
PlaneFilter.groundVehicles = groundFilter;
|
|
}
|
|
|
|
function filterBlockedMLAT(switchFilter) {
|
|
if (typeof loStore['blockedMLATFilter'] === 'undefined') {
|
|
loStore['blockedMLATFilter'] = 'not_filtered';
|
|
}
|
|
let blockedMLATFilter = loStore['blockedMLATFilter'];
|
|
if (switchFilter === true) {
|
|
blockedMLATFilter = (blockedMLATFilter === 'not_filtered') ? 'filtered' : 'not_filtered';
|
|
}
|
|
if (blockedMLATFilter === 'not_filtered') {
|
|
jQuery('#blockedmlat_filter').addClass('settingsCheckboxChecked');
|
|
} else {
|
|
jQuery('#blockedmlat_filter').removeClass('settingsCheckboxChecked');
|
|
}
|
|
loStore['blockedMLATFilter'] = blockedMLATFilter;
|
|
PlaneFilter.blockedMLAT = blockedMLATFilter;
|
|
}
|
|
|
|
function buttonActive(id, state) {
|
|
if (state) {
|
|
jQuery(id).addClass('activeButton');
|
|
jQuery(id).removeClass('inActiveButton');
|
|
} else {
|
|
jQuery(id).addClass('inActiveButton');
|
|
jQuery(id).removeClass('activeButton');
|
|
}
|
|
}
|
|
|
|
function toggleIsolation(state) {
|
|
let prevState = onlySelected;
|
|
if (showTrace && state !== "on")
|
|
return;
|
|
onlySelected = !onlySelected;
|
|
if (state === "on")
|
|
onlySelected = true;
|
|
if (state === "off")
|
|
onlySelected = false;
|
|
|
|
buttonActive('#I', onlySelected);
|
|
|
|
if (prevState != onlySelected)
|
|
refreshFilter();
|
|
}
|
|
|
|
function toggleMilitary() {
|
|
onlyMilitary = !onlyMilitary;
|
|
buttonActive('#U', onlyMilitary);
|
|
|
|
refreshFilter();
|
|
active();
|
|
if (onlyMilitary)
|
|
fetchData({force: true});
|
|
}
|
|
|
|
function togglePersistence() {
|
|
noVanish = !noVanish;
|
|
//filterTracks = noVanish;
|
|
|
|
buttonActive('#P', noVanish);
|
|
|
|
remakeTrails();
|
|
|
|
if (!noVanish)
|
|
reaper();
|
|
loStore['noVanish'] = noVanish;
|
|
console.log('noVanish = ' + noVanish);
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
function dim(evt) {
|
|
try {
|
|
const dim = mapDimPercentage * (1 + 0.25 * toggles['darkerColors'].state) + layerExtraDim;
|
|
const contrast = mapContrastPercentage * (1 + 0.1 * toggles['darkerColors'].state) + layerExtraContrast;
|
|
if (dim > 0.0001) {
|
|
evt.context.globalCompositeOperation = 'multiply';
|
|
evt.context.fillStyle = 'rgba(0,0,0,'+dim+')';
|
|
evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
|
|
} else if (dim < -0.0001) {
|
|
evt.context.globalCompositeOperation = 'screen';
|
|
console.log(evt.context.globalCompositeOperation);
|
|
evt.context.fillStyle = 'rgba(255, 255, 255,'+(-dim)+')';
|
|
evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
|
|
}
|
|
if (contrast > 0.0001) {
|
|
evt.context.globalCompositeOperation = 'overlay';
|
|
evt.context.fillStyle = 'rgba(0,0,0,'+contrast+')';
|
|
evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
|
|
} else if (contrast < -0.0001) {
|
|
evt.context.globalCompositeOperation = 'overlay';
|
|
evt.context.fillStyle = 'rgba(255, 255, 255,'+ (-contrast)+')';
|
|
evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height);
|
|
}
|
|
evt.context.globalCompositeOperation = 'source-over';
|
|
} catch (error) {
|
|
console.error(error);
|
|
}
|
|
}
|
|
function invertMap(evt){
|
|
const ctx=evt.context;
|
|
ctx.globalCompositeOperation='difference';
|
|
ctx.fillStyle = "white";
|
|
ctx.globalAlpha = alpha; // alpha 0 = no effect 1 = full effect
|
|
ctx.fillRect(0, 0, evt.ctx.canvas.width, ctx.canvas.height);
|
|
|
|
|
|
}
|
|
//
|
|
// Altitude Chart begin
|
|
//
|
|
(function (global, jQuery, TAR) {
|
|
let altitudeChart = TAR.altitudeChart = TAR.altitudeChart || {};
|
|
|
|
function createLegendGradientStops() {
|
|
const mapOffsetToAltitude = [[0.033, 500], [0.066, 1000], [0.126, 2000], [0.19, 4000], [0.253, 6000], [0.316, 8000], [0.38, 10000], [0.59, 20000], [0.79, 30000], [1, 40000]];
|
|
|
|
let stops = '';
|
|
for (let i in mapOffsetToAltitude) {
|
|
let map = mapOffsetToAltitude[i];
|
|
const color = altitudeColor(map[1]);
|
|
stops += '<stop offset="' + map[0] + '" stop-color="hsl(' + color[0] + ',' + color[1] + '%,' + color[2] + '%)" />';
|
|
}
|
|
return stops;
|
|
}
|
|
|
|
function createLegendUrl(data) {
|
|
jQuery(data).find('#linear-gradient').html(createLegendGradientStops());
|
|
|
|
const svg = jQuery('svg', data).prop('outerHTML');
|
|
|
|
return 'url("data:image/svg+xml;base64,' + global.btoa(svg) + '")';
|
|
}
|
|
|
|
function loadLegend() {
|
|
let baseLegend = (DisplayUnits === 'metric') ? 'images/alt_legend_m.svg' : 'images/alt_legend_ft.svg';
|
|
|
|
jQuery.get(baseLegend, function (data) {
|
|
jQuery('#altitude_chart_button').css("background-image", createLegendUrl(data));
|
|
});
|
|
}
|
|
|
|
altitudeChart.render = function () {
|
|
if (toggles['altitudeChart'].state) {
|
|
loadLegend();
|
|
jQuery('#altitude_chart').show();
|
|
} else {
|
|
jQuery('#altitude_chart').hide();
|
|
}
|
|
}
|
|
|
|
altitudeChart.init = function () {
|
|
new Toggle({
|
|
key: "altitudeChart",
|
|
display: "Altitude Chart",
|
|
container: "#settingsRight",
|
|
init: (onMobile ? false : true),
|
|
setState: altitudeChart.render
|
|
});
|
|
}
|
|
|
|
return TAR;
|
|
}(window, jQuery, TAR || {}));
|
|
//
|
|
// Altitude Chart end
|
|
//
|
|
|
|
function followRandomPlane() {
|
|
if (showTrace)
|
|
return;
|
|
let this_one = null;
|
|
let tired = 0;
|
|
do {
|
|
this_one = PlanesOrdered[Math.floor(Math.random()*PlanesOrdered.length)];
|
|
if (!this_one || tired++ > 1000)
|
|
break;
|
|
} while ((this_one.isFiltered() && !onlySelected) || !this_one.position || (now - this_one.position_time > 30));
|
|
//console.log(this_one.icao);
|
|
if (this_one)
|
|
selectPlaneByHex(this_one.icao, {follow: true});
|
|
}
|
|
|
|
function toggleTableInView(switchOn) {
|
|
if (switchOn || (globeIndex && !icaoFilter)) {
|
|
tableInView = true;
|
|
} else {
|
|
tableInView = !tableInView;
|
|
TAR.planeMan.refresh();
|
|
}
|
|
loStore['tableInView'] = tableInView;
|
|
|
|
jQuery('#with_positions').text(tableInView ? "On Screen:" : "With Position:");
|
|
|
|
buttonActive('#V', tableInView);
|
|
}
|
|
|
|
function toggleLabels() {
|
|
enableLabels = !enableLabels;
|
|
loStore['enableLabels'] = enableLabels;
|
|
for (let key in PlanesOrdered) {
|
|
PlanesOrdered[key].updateMarker();
|
|
}
|
|
refreshFeatures();
|
|
buttonActive('#L', enableLabels);
|
|
|
|
if (showTrace)
|
|
remakeTrails();
|
|
}
|
|
|
|
function toggleExtendedLabels(options) {
|
|
if (isNaN(extendedLabels))
|
|
extendedLabels = 0;
|
|
|
|
options = options || {};
|
|
if (!options.noIncrement) {
|
|
extendedLabels++;
|
|
}
|
|
extendedLabels %= 4;
|
|
//console.log(extendedLabels);
|
|
loStore['extendedLabels'] = extendedLabels;
|
|
for (let key in PlanesOrdered) {
|
|
PlanesOrdered[key].updateMarker();
|
|
}
|
|
buttonActive('#O', extendedLabels);
|
|
}
|
|
|
|
function toggleTrackLabels() {
|
|
trackLabels = !trackLabels;
|
|
loStore['trackLabels'] = trackLabels;
|
|
|
|
remakeTrails();
|
|
|
|
buttonActive('#K', trackLabels);
|
|
}
|
|
|
|
function toggleMultiSelect(newState) {
|
|
let prevState = multiSelect;
|
|
multiSelect = !multiSelect;
|
|
|
|
if (newState == "on")
|
|
multiSelect = true;
|
|
if (newState == "off")
|
|
multiSelect = false;
|
|
|
|
if (!multiSelect) {
|
|
if (!SelectedPlane)
|
|
toggleIsolation("off");
|
|
if (prevState != multiSelect)
|
|
deselectAllPlanes("keepMain");
|
|
}
|
|
|
|
buttonActive('#M', multiSelect);
|
|
}
|
|
|
|
function onJump(e) {
|
|
toggleFollow(false);
|
|
if (e) {
|
|
e.preventDefault();
|
|
onJumpInput = jQuery("#jump_input").val();
|
|
jQuery("#jump_input").val("");
|
|
jQuery("#jump_input").blur();
|
|
}
|
|
let coords = null;
|
|
let airport = null;
|
|
if (onJumpInput.indexOf(",") >= 0) {
|
|
let values = onJumpInput.split(',');
|
|
if (!values || values.length != 2) {
|
|
showSearchWarning('Input format decimal coordinates: LATI.TUDE, LONGI.TUDE');
|
|
}
|
|
coords = [parseFloat(values[0]), parseFloat(values[1])];
|
|
} else {
|
|
airport = onJumpInput.trim().toUpperCase();
|
|
}
|
|
if (airport) {
|
|
if (!_airport_coords_cache) {
|
|
jQuery.getJSON(databaseFolder + "/airport-coords.js")
|
|
.done(function(data) {
|
|
_airport_coords_cache = data;
|
|
onJump();
|
|
});
|
|
return;
|
|
}
|
|
coords = _airport_coords_cache[airport];
|
|
}
|
|
if (coords) {
|
|
console.log("jumping to: " + coords[0] + " " + coords[1]);
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([coords[1], coords[0]]));
|
|
|
|
if (zoomLvl >= 7) {
|
|
fetchData({force: true});
|
|
}
|
|
|
|
refreshFilter();
|
|
hideSearchWarning();
|
|
} else {
|
|
showSearchWarning('Failed to find airport ' + airport);
|
|
}
|
|
}
|
|
|
|
function hideSearchWarning() {
|
|
const searchWarning = jQuery('#search_warning');
|
|
if (searchWarning.css('display') !== 'none') {
|
|
searchWarning.hide('slow');
|
|
}
|
|
}
|
|
|
|
function showSearchWarning(message) {
|
|
const searchWarning = jQuery('#search_warning');
|
|
searchWarning.text(message)
|
|
if (searchWarning.css('display') === 'none') {
|
|
searchWarning.show();
|
|
}
|
|
|
|
//auto hide after 15 seconds
|
|
setTimeout(() => hideSearchWarning(), 15000);
|
|
}
|
|
|
|
function onSearch(e) {
|
|
e.preventDefault();
|
|
const searchTerm = jQuery("#search_input").val().trim();
|
|
jQuery("#search_input").val("");
|
|
jQuery("#search_input").blur();
|
|
let results = [];
|
|
if (searchTerm)
|
|
results = findPlanes(searchTerm, "byIcao", "byCallsign", "byReg", "byType", true);
|
|
if (results.length > 0 && globeIndex) {
|
|
toggleIsolation("on");
|
|
if (results.length < 100) {
|
|
getTrace(null, null, {list: results});
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function onSearchClear(e) {
|
|
deselectAllPlanes();
|
|
toggleIsolation("off");
|
|
toggleMultiSelect("off");
|
|
jQuery("#search_input").val("");
|
|
jQuery("#search_input").blur();
|
|
}
|
|
|
|
function onResetCallsignFilter(e) {
|
|
jQuery("#callsign_filter").val("");
|
|
jQuery("#callsign_filter").blur();
|
|
|
|
updateCallsignFilter();
|
|
}
|
|
|
|
function updateCallsignFilter(e) {
|
|
if (e)
|
|
e.preventDefault();
|
|
|
|
jQuery("#callsign_filter").blur();
|
|
|
|
PlaneFilter.callsign = jQuery("#callsign_filter").val().trim().toUpperCase();
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
function onResetTypeFilter(e) {
|
|
jQuery("#type_filter").val("");
|
|
jQuery("#type_filter").blur();
|
|
|
|
updateTypeFilter();
|
|
}
|
|
|
|
function updateTypeFilter(e) {
|
|
if (e)
|
|
e.preventDefault();
|
|
|
|
jQuery("#type_filter").blur();
|
|
let type = jQuery("#type_filter").val().trim();
|
|
|
|
PlaneFilter.type = type.toUpperCase();
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
function onResetIcaoFilter(e) {
|
|
jQuery("#icao_filter").val("");
|
|
jQuery("#icao_filter").blur();
|
|
|
|
updateIcaoFilter();
|
|
}
|
|
|
|
function updateIcaoFilter(e) {
|
|
if (e)
|
|
e.preventDefault();
|
|
|
|
jQuery("#icao_filter").blur();
|
|
let icao = jQuery("#icao_filter").val().trim();
|
|
|
|
PlaneFilter.icao = icao.toLowerCase();
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
function onResetDescriptionFilter(e) {
|
|
jQuery("#description_filter").val("");
|
|
jQuery("#description_filter").blur();
|
|
|
|
updateTypeFilter();
|
|
}
|
|
|
|
function updateDescriptionFilter(e) {
|
|
if (e)
|
|
e.preventDefault();
|
|
|
|
jQuery("#description_filter").blur();
|
|
let description = jQuery("#description_filter").val().trim();
|
|
|
|
PlaneFilter.description = description.toUpperCase();
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
function onResetAltitudeFilter(e) {
|
|
jQuery("#altitude_filter_min").val("");
|
|
jQuery("#altitude_filter_max").val("");
|
|
jQuery("#altitude_filter_min").blur();
|
|
jQuery("#altitude_filter_max").blur();
|
|
|
|
updateAltFilter();
|
|
refreshFilter();
|
|
}
|
|
|
|
function updateAltFilter() {
|
|
let minAltitude = parseFloat(jQuery("#altitude_filter_min").val().trim());
|
|
let maxAltitude = parseFloat(jQuery("#altitude_filter_max").val().trim());
|
|
let enabled = false;
|
|
|
|
if (minAltitude < -1e6 || minAltitude > 1e6 || isNaN(minAltitude))
|
|
minAltitude = -1e6;
|
|
else
|
|
enabled = true;
|
|
if (maxAltitude < -1e6 || maxAltitude > 1e6 || isNaN(maxAltitude))
|
|
maxAltitude = 1e6;
|
|
else
|
|
enabled = true;
|
|
|
|
if (!enabled) {
|
|
PlaneFilter.enabled = false;
|
|
PlaneFilter.minAltitude = undefined;
|
|
PlaneFilter.maxAltitude = undefined;
|
|
}
|
|
|
|
PlaneFilter.enabled = enabled;
|
|
|
|
if (DisplayUnits == "metric") {
|
|
PlaneFilter.minAltitude = minAltitude * 3.2808;
|
|
PlaneFilter.maxAltitude = maxAltitude * 3.2808;
|
|
} else {
|
|
PlaneFilter.minAltitude = minAltitude;
|
|
PlaneFilter.maxAltitude = maxAltitude;
|
|
}
|
|
}
|
|
|
|
function getFlightAwareIdentLink(ident, linkText) {
|
|
if (ident !== null && ident !== "") {
|
|
if (!linkText) {
|
|
linkText = ident;
|
|
}
|
|
return '<a class="link" target="_blank" href="https://flightaware.com/live/flight/' + ident.trim() + '" rel="noreferrer">' + linkText + '</a>';
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function onResetSourceFilter(e) {
|
|
jQuery('#sourceFilter .ui-selected').removeClass('ui-selected');
|
|
|
|
sourcesFilter = null;
|
|
|
|
updateSourceFilter();
|
|
}
|
|
|
|
function updateSourceFilter(e) {
|
|
if (e)
|
|
e.preventDefault();
|
|
|
|
PlaneFilter.sources = sourcesFilter;
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
function onResetFlagFilter(e) {
|
|
jQuery('#flagFilter .ui-selected').removeClass('ui-selected');
|
|
|
|
flagFilter = null;
|
|
|
|
updateFlagFilter();
|
|
}
|
|
|
|
function updateFlagFilter(e) {
|
|
if (e)
|
|
e.preventDefault();
|
|
|
|
PlaneFilter.flagFilter = flagFilter;
|
|
|
|
refreshFilter();
|
|
}
|
|
|
|
|
|
function getFlightAwareModeSLink(code, ident, linkText) {
|
|
if (code !== null && code.length > 0 && code[0] !== '~' && code !== "000000") {
|
|
if (!linkText) {
|
|
linkText = "FlightAware: " + code.toUpperCase();
|
|
}
|
|
|
|
let linkHtml = "<a class=\"link\" target=\"_blank\" href=\"https://flightaware.com/live/modes/" + code ;
|
|
if (ident != null && ident !== "") {
|
|
linkHtml += "/ident/" + ident.trim();
|
|
}
|
|
linkHtml += "/redirect\" rel=\"noreferrer\">" + linkText + "</a>";
|
|
return linkHtml;
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function getPhotoLink(ac) {
|
|
if (jetphotoLinks) {
|
|
if (ac.registration == null || ac.registration == "")
|
|
return "";
|
|
return "<a class=\"link\" target=\"_blank\" href=\"https://www.jetphotos.com/photo/keyword/" + ac.registration.replace(/[^0-9a-z]/ig,'') + "\" rel=\"noreferrer\">Jetphotos</a>";
|
|
} else if (flightawareLinks) {
|
|
if (ac.registration == null || ac.registration == "")
|
|
return "";
|
|
return "<a class=\"link\" target=\"_blank\" href=\"https://flightaware.com/photos/aircraft/" + ac.registration.replace(/[^0-9a-z]/ig,'') + "\" rel=\"noreferrer\">FA Photos</a>";
|
|
} else if (showPictures) {
|
|
return "<a class=\"link\" target=\"_blank\" href=\"https://www.planespotters.net/hex/" + ac.icao.toUpperCase() + "\" rel=\"noreferrer\">View on Planespotters</a>";
|
|
}
|
|
}
|
|
|
|
// takes in an elemnt jQuery path and the OL3 layer name and toggles the visibility based on clicking it
|
|
function toggleLayer(element, layer) {
|
|
// set initial checked status
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (lyr.get('name') === layer && lyr.getVisible()) {
|
|
jQuery(element).addClass('settingsCheckboxChecked');
|
|
}
|
|
});
|
|
jQuery(element).on('click', function() {
|
|
let visible = false;
|
|
if (jQuery(element).hasClass('settingsCheckboxChecked')) {
|
|
visible = true;
|
|
}
|
|
ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) {
|
|
if (lyr.get('name') === layer) {
|
|
if (visible) {
|
|
lyr.setVisible(false);
|
|
jQuery(element).removeClass('settingsCheckboxChecked');
|
|
} else {
|
|
lyr.setVisible(true);
|
|
jQuery(element).addClass('settingsCheckboxChecked');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
let fetchingPf = false;
|
|
function fetchPfData() {
|
|
if (fetchingPf)
|
|
return;
|
|
fetchingPf = true;
|
|
for (let i in pf_data) {
|
|
const req = jQuery.ajax({ url: pf_data[i],
|
|
dataType: 'json' });
|
|
jQuery.when(req).done(function(data) {
|
|
for (let i in PlanesOrdered) {
|
|
const plane = PlanesOrdered[i];
|
|
const ac = data.aircraft[plane.icao.toUpperCase()];
|
|
if (!ac) {
|
|
continue;
|
|
}
|
|
plane.pfRoute = ac.route;
|
|
plane.pfMach = ac.mach;
|
|
plane.pfFlightno = ac.flightno;
|
|
if (!plane.registration && ac.reg && ac.reg != "????" && ac.reg != "z.NO-REG")
|
|
plane.registration = ac.reg;
|
|
if (!plane.icaoType && ac.type && ac.type != "????" && ac.type != "ZVEH") {
|
|
plane.icaoType = ac.type;
|
|
plane.setTypeData();
|
|
}
|
|
}
|
|
fetchingPf = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
function solidGoldT(arg) {
|
|
solidT = true;
|
|
let list = [[], [], [], []];
|
|
for (let i = 0; i < PlanesOrdered.length; i++) {
|
|
let plane = PlanesOrdered[i];
|
|
//console.log(plane);
|
|
if (plane.visible) {
|
|
list[Math.floor(4*i/PlanesOrdered.length)].push(plane);
|
|
}
|
|
}
|
|
getTrace(null, null, {onlyRecent: arg == 2, onlyFull: arg == 1, list: list[0],});
|
|
getTrace(null, null, {onlyRecent: arg == 2, onlyFull: arg == 1, list: list[1],});
|
|
getTrace(null, null, {onlyRecent: arg == 2, onlyFull: arg == 1, list: list[2],});
|
|
getTrace(null, null, {onlyRecent: arg == 2, onlyFull: arg == 1, list: list[3],});
|
|
}
|
|
|
|
function bearingFromLonLat(position1, position2) {
|
|
// Positions in format [lon in deg, lat in deg]
|
|
const lon1 = position1[0]*Math.PI/180;
|
|
const lat1 = position1[1]*Math.PI/180;
|
|
const lon2 = position2[0]*Math.PI/180;
|
|
const lat2 = position2[1]*Math.PI/180;
|
|
|
|
const y = Math.sin(lon2-lon1)*Math.cos(lat2);
|
|
const x = Math.cos(lat1)*Math.sin(lat2)
|
|
- Math.sin(lat1)*Math.cos(lat2)*Math.cos(lon2-lon1);
|
|
return (Math.atan2(y, x)* 180 / Math.PI + 360) % 360;
|
|
}
|
|
|
|
function zoomIn() {
|
|
const zoom = OLMap.getView().getZoom();
|
|
OLMap.getView().setZoom((zoom+1).toFixed());
|
|
if (FollowSelected)
|
|
toggleFollow(true);
|
|
}
|
|
|
|
function zoomOut() {
|
|
const zoom = OLMap.getView().getZoom();
|
|
OLMap.getView().setZoom((zoom-1).toFixed());
|
|
if (FollowSelected)
|
|
toggleFollow(true);
|
|
}
|
|
|
|
function changeZoom(init) {
|
|
if (!OLMap)
|
|
return;
|
|
|
|
zoomLvl = OLMap.getView().getZoom();
|
|
|
|
checkScale();
|
|
|
|
// small zoomstep, no need to change aircraft scaling
|
|
if (!init && Math.abs(zoomLvl-zoomLvlCache) < 0.4)
|
|
return;
|
|
|
|
loStore['zoomLvl'] = zoomLvl;
|
|
zoomLvlCache = zoomLvl;
|
|
|
|
if (!init && showTrace)
|
|
updateAddressBar();
|
|
|
|
checkPointermove();
|
|
}
|
|
|
|
function checkScale() {
|
|
if (zoomLvl > markerZoomDivide)
|
|
iconSize = markerBig;
|
|
else
|
|
iconSize = markerSmall;
|
|
|
|
// scale markers according to global scaling
|
|
iconSize *= Math.pow(1.3, globalScale) * globalScale * iconScale;
|
|
// disable, doesn't work well
|
|
// iconSize *= 1 - 0.37 * Math.pow(TrackedAircraftPositions + 1, 0.8) / Math.pow(10000, 0.8);
|
|
}
|
|
function setGlobalScale(scale, init) {
|
|
globalScale = scale;
|
|
document.documentElement.style.setProperty("--SCALE", globalScale);
|
|
|
|
labelFont = "bold " + (12 * globalScale * labelScale) + "px/" + (14 * globalScale * labelScale) + "px Tahoma, Verdana, Helvetica, sans-serif";
|
|
|
|
checkScale();
|
|
setLineWidth();
|
|
if (!init) {
|
|
refreshFeatures();
|
|
refreshSelected();
|
|
refreshHighlighted();
|
|
remakeTrails();
|
|
}
|
|
}
|
|
|
|
function checkPointermove() {
|
|
if ((webgl || zoomLvl > 5.5) && enableMouseover && !onMobile) {
|
|
OLMap.on('pointermove', onPointermove);
|
|
} else {
|
|
OLMap.un('pointermove', onPointermove);
|
|
removeHighlight();
|
|
}
|
|
}
|
|
|
|
|
|
function changeCenter(init) {
|
|
const rawCenter = OLMap.getView().getCenter();
|
|
const center = ol.proj.toLonLat(rawCenter);
|
|
|
|
const centerChanged = (Math.abs(center[1] - CenterLat) > 0.000001 || Math.abs(center[0] - CenterLon) > 0.000001);
|
|
|
|
if (!init && !centerChanged) {
|
|
return;
|
|
}
|
|
|
|
loStore['CenterLon'] = CenterLon = center[0];
|
|
loStore['CenterLat'] = CenterLat = center[1];
|
|
|
|
if (!init) {
|
|
updateAddressBar();
|
|
}
|
|
|
|
if (rawCenter[0] < OLProjExtent[0] || rawCenter[0] > OLProjExtent[2]) {
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat(center));
|
|
mapRefresh();
|
|
}
|
|
if (CenterLat < -85)
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([center[0], -85]));
|
|
if (CenterLat > 85)
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([center[0], 85]));
|
|
}
|
|
|
|
let lastMovement = 0;
|
|
let checkMoveZoom;
|
|
let checkMoveCenter = [0, 0];
|
|
let checkMoveDone = 0;
|
|
|
|
function checkMovement() {
|
|
if (!OLMap)
|
|
return;
|
|
const zoom = OLMap.getView().getZoom();
|
|
const center = ol.proj.toLonLat(OLMap.getView().getCenter());
|
|
const ts = new Date().getTime();
|
|
|
|
if (
|
|
checkMoveZoom != zoom ||
|
|
checkMoveCenter[0] != center[0] ||
|
|
checkMoveCenter[1] != center[1]
|
|
) {
|
|
checkMoveDone = 0;
|
|
if (FollowSelected) {
|
|
checkFollow();
|
|
}
|
|
active();
|
|
lastMovement = ts;
|
|
}
|
|
|
|
checkMoveZoom = zoom;
|
|
checkMoveCenter[0] = center[0];
|
|
checkMoveCenter[1] = center[1];
|
|
|
|
changeZoom();
|
|
changeCenter();
|
|
|
|
const elapsed = Math.abs(ts - lastMovement);
|
|
|
|
if (!checkMoveDone && heatmap && elapsed > 300) {
|
|
if (!heatmap.manualRedraw)
|
|
drawHeatmap();
|
|
checkMoveDone = 1;
|
|
}
|
|
if (elapsed > 500 || (!onMobile && elapsed > 45)) {
|
|
checkRefresh();
|
|
}
|
|
|
|
fetchData();
|
|
}
|
|
|
|
function getZoom() {
|
|
return OLMap.getView().getZoom();
|
|
}
|
|
|
|
function getCenter() {
|
|
return ol.proj.toLonLat(OLMap.getView().getCenter());
|
|
}
|
|
|
|
let lastRefresh = 0;
|
|
let refreshZoom, refreshCenter;
|
|
function checkRefresh() {
|
|
if (showTrace)
|
|
return;
|
|
if (triggerRefresh) {
|
|
refresh();
|
|
return;
|
|
}
|
|
const center = getCenter();
|
|
const zoom = getZoom();
|
|
if (zoom != refreshZoom || !refreshCenter || center[0] != refreshCenter[0] || center[1] != refreshCenter[1]) {
|
|
const ts = new Date().getTime();
|
|
const elapsed = Math.abs(ts - lastRefresh);
|
|
let num = Math.min(1500, Math.max(250, TrackedAircraftPositions / 300 * 250));
|
|
if (elapsed > num) {
|
|
refresh();
|
|
}
|
|
}
|
|
}
|
|
function refresh() {
|
|
lastRefresh = new Date().getTime();
|
|
|
|
refreshZoom = getZoom();
|
|
refreshCenter = getCenter();
|
|
|
|
if (replay) {
|
|
for (let i in SelPlanes) {
|
|
const plane = SelPlanes[i];
|
|
plane.processTrace();
|
|
}
|
|
}
|
|
|
|
//console.time("refreshTable");
|
|
TAR.planeMan.refresh();
|
|
//console.timeEnd("refreshTable");
|
|
mapRefresh();
|
|
|
|
triggerRefresh = 0;
|
|
|
|
refreshSelected();
|
|
refreshHighlighted();
|
|
}
|
|
|
|
function mapRefresh(redraw) {
|
|
if (!mapIsVisible || heatmap)
|
|
return;
|
|
//console.log('mapRefresh()');
|
|
let addToMap = [];
|
|
let nMapPlanes = 0;
|
|
if (globeIndex && !icaoFilter) {
|
|
for (let i in PlanesOrdered) {
|
|
const plane = PlanesOrdered[i];
|
|
delete plane.glMarker;
|
|
// disable mobile limitations when using webGL
|
|
if (
|
|
(!onMobile || webgl || nMapPlanes < 150)
|
|
&& (!onMobile || webgl || zoomLvl > 10 || !plane.onGround)
|
|
&& plane.visible
|
|
&& plane.inView
|
|
) {
|
|
addToMap.push(plane);
|
|
nMapPlanes++;
|
|
} else if (plane.selected) {
|
|
addToMap.push(plane);
|
|
nMapPlanes++;
|
|
} else {
|
|
plane.markerDrawn && plane.clearMarker();
|
|
plane.linesDrawn && plane.clearLines();
|
|
}
|
|
}
|
|
} else {
|
|
for (let i in PlanesOrdered) {
|
|
const plane = PlanesOrdered[i];
|
|
addToMap.push(plane);
|
|
delete plane.glMarker;
|
|
}
|
|
}
|
|
|
|
// webGL zIndex hack:
|
|
// sort all planes by altitude
|
|
// clear the vector source
|
|
// delete all feature objects so they are recreated, this is important
|
|
// draw order will be insertion / updateFeatures / updateTick order
|
|
|
|
addToMap.sort(function(x, y) { return x.zIndex - y.zIndex; });
|
|
//console.log('maprefresh(): ' + addToMap.length);
|
|
if (webgl) {
|
|
webglFeatures.clear();
|
|
}
|
|
if (globeIndex && !icaoFilter) {
|
|
for (let i in addToMap) {
|
|
addToMap[i].updateFeatures(redraw);
|
|
}
|
|
} else {
|
|
for (let i in addToMap) {
|
|
addToMap[i].updateTick(redraw);
|
|
}
|
|
}
|
|
}
|
|
|
|
function onPointermove(evt) {
|
|
//clearTimeout(pointerMoveTimeout);
|
|
//pointerMoveTimeout = setTimeout(highlight(evt), 100);
|
|
highlight(evt);
|
|
}
|
|
|
|
function highlight(evt) {
|
|
const hex = evt.map.forEachFeatureAtPixel(evt.pixel,
|
|
function(feature, layer) {
|
|
return feature.hex;
|
|
},
|
|
{
|
|
layerFilter: function(layer) {
|
|
return (layer == iconLayer || layer == webglLayer);
|
|
},
|
|
hitTolerance: 5 * globalScale,
|
|
}
|
|
);
|
|
|
|
if (HighlightedPlane && hex == HighlightedPlane.icao)
|
|
return;
|
|
|
|
//clearTimeout(pointerMoveTimeout);
|
|
|
|
if (hex) {
|
|
HighlightedPlane = Planes[hex];
|
|
} else {
|
|
HighlightedPlane = null;
|
|
}
|
|
//pointerMoveTimeout = setTimeout(refreshHighlighted(), 300);
|
|
refreshHighlighted();
|
|
}
|
|
let urlIcaos = [];
|
|
function parseURLIcaos() {
|
|
if (usp.has('icao')) {
|
|
let inArray = usp.get('icao').toLowerCase().split(',');
|
|
for (let i = 0; i < inArray.length; i++) {
|
|
const icao = inArray[i].toLowerCase();
|
|
if (icao && (icao.length == 7 || icao.length == 6) && icao.toLowerCase().match(/[a-f,0-9]{6}/)) {
|
|
urlIcaos.push(icao);
|
|
let newPlane = Planes[icao] || new PlaneObject(icao);
|
|
newPlane.last_message_time = NaN;
|
|
newPlane.position_time = NaN;
|
|
newPlane.selected = true;
|
|
SelPlanes.push(newPlane);
|
|
//console.log(newPlane);
|
|
// preliminary adding of URL specified icaos
|
|
}
|
|
}
|
|
}
|
|
}
|
|
function processURLParams(){
|
|
if (usp.has('showTrace')) {
|
|
let date = setTraceDate({string: usp.get('showTrace')});
|
|
if (date && usp.has('startTime')) {
|
|
let numbers = usp.get('startTime').split(':');
|
|
traceOpts.startHours = numbers[0] ? parseInt(numbers[0]) : 0;
|
|
traceOpts.startMinutes = numbers[1] ? parseInt(numbers[1]) : 0;
|
|
traceOpts.startSeconds = numbers[2] ? parseInt(numbers[2]) : 0;
|
|
}
|
|
if (date && usp.has('endTime')) {
|
|
let numbers = usp.get('endTime').split(':');
|
|
traceOpts.endHours = numbers[0] ? parseInt(numbers[0]) : 24;
|
|
traceOpts.endMinutes = numbers[1] ? parseInt(numbers[1]) : 0;
|
|
traceOpts.endSeconds = numbers[2] ? parseInt(numbers[2]) : 0;
|
|
}
|
|
if (date && usp.getFloat('timestamp')) {
|
|
showTraceTimestamp = usp.getFloat('timestamp');
|
|
}
|
|
}
|
|
|
|
const callsign = usp.get('callsign');
|
|
let zoom = null;
|
|
let follow = true;
|
|
if (usp.get("zoom")) {
|
|
try {
|
|
zoom = parseFloat(usp.get("zoom"));
|
|
if (zoom === 0)
|
|
zoom = 8;
|
|
} catch (error) {
|
|
console.log("Error parsing zoom:", error);
|
|
}
|
|
}
|
|
|
|
if (usp.get("lat") && usp.get("lon")) {
|
|
try {
|
|
const lat = parseFloat(usp.get("lat"));
|
|
const lon = parseFloat(usp.get("lon"));
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([lon, lat]));
|
|
follow = false;
|
|
traceOpts.noFollow = new Date().getTime() / 1000;
|
|
}
|
|
catch (error) {
|
|
console.log("Error parsing lat/lon:", error);
|
|
}
|
|
}
|
|
|
|
if (urlIcaos.length > 0) {
|
|
const icaos = urlIcaos;
|
|
if (!usp.has('noIsolation'))
|
|
toggleIsolation("on");
|
|
if (icaos.length > 1) {
|
|
toggleMultiSelect("on");
|
|
//follow = false;
|
|
}
|
|
for (let i = 0; i < icaos.length; i++) {
|
|
const icao = icaos[i];
|
|
console.log('Selected ICAO id: '+ icao + ' traceDate: ' + traceDateString);
|
|
let options = {follow: follow, noDeselect: true};
|
|
if (traceDate != null) {
|
|
let newPlane = Planes[icao] || new PlaneObject(icao);
|
|
newPlane.last_message_time = NaN;
|
|
newPlane.position_time = NaN;
|
|
newPlane.selected = true;
|
|
select(newPlane, options);
|
|
|
|
if (!zoom)
|
|
zoom = 5;
|
|
} else {
|
|
if (!zoom)
|
|
zoom = 7;
|
|
selectPlaneByHex(icao, options)
|
|
}
|
|
}
|
|
if (traceDate != null)
|
|
{
|
|
toggleShowTrace();
|
|
toggleFollow(follow);
|
|
}
|
|
updateAddressBar();
|
|
} else if (callsign != null) {
|
|
findPlanes(callsign, false, true, false, false, false);
|
|
}
|
|
|
|
if (zoom) {
|
|
OLMap.getView().setZoom(zoom);
|
|
}
|
|
|
|
if (usp.has('mil'))
|
|
toggleMilitary();
|
|
|
|
if (usp.has('airport')) {
|
|
onJumpInput = usp.get('airport').trim().toUpperCase();
|
|
onJump();
|
|
}
|
|
|
|
if (usp.has('leg')) {
|
|
legSel = parseInt(usp.get('leg'), 10);
|
|
if (isNaN(legSel) || legSel < -1)
|
|
legSel = -1;
|
|
else
|
|
legSel--;
|
|
}
|
|
|
|
let tracks = usp.get('monochromeTracks');
|
|
if (tracks != undefined) {
|
|
if (tracks.length == 6)
|
|
monochromeTracks = '#' + tracks;
|
|
else
|
|
monochromeTracks = "#000000";
|
|
}
|
|
|
|
let markers = usp.get('monochromeMarkers');
|
|
if (markers != undefined) {
|
|
if (markers.length == 6)
|
|
monochromeMarkers = '#' + markers;
|
|
else
|
|
monochromeMarkers = "#FFFFFF";
|
|
}
|
|
|
|
let outlineColor = usp.get('outlineColor');
|
|
if (outlineColor != undefined) {
|
|
if (outlineColor.length == 6)
|
|
OutlineADSBColor = '#' + outlineColor;
|
|
else
|
|
OutlineADSBColor = "#000000";
|
|
}
|
|
|
|
if (usp.has('centerReceiver')) {
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([SiteLon, SiteLat]));
|
|
}
|
|
if (usp.has('lockDotCentered')) {
|
|
lockDotCentered = true;
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([SiteLon, SiteLat]));
|
|
}
|
|
}
|
|
|
|
let regIcaoDownloadRunning = false;
|
|
function regIcaoDownload(opts) {
|
|
regIcaoDownloadRunning = true;
|
|
let req = jQuery.ajax({ url: databaseFolder + "/regIcao.js",
|
|
cache: true,
|
|
timeout: 60000,
|
|
dataType : 'json',
|
|
opts: opts,
|
|
});
|
|
req.done(function(data) {
|
|
regCache = data;
|
|
});
|
|
req.always(function() {
|
|
regIcaoDownloadRunning = false;
|
|
});
|
|
return req;
|
|
}
|
|
function findPlanes(queries, byIcao, byCallsign, byReg, byType, showWarnings) {
|
|
if (queries == null)
|
|
return;
|
|
queries = queries.toLowerCase();
|
|
queries = queries.split(',');
|
|
if (queries.length > 1)
|
|
toggleMultiSelect("on");
|
|
let results = [];
|
|
for (let i in queries) {
|
|
const query = queries[i];
|
|
if (byReg) {
|
|
let upper = query.toUpperCase().replace("-", "");
|
|
if (regCache) {
|
|
if (regCache[upper]) {
|
|
selectPlaneByHex(regCache[upper].toLowerCase(), {noDeselect: true, follow: true});
|
|
}
|
|
} else if (!regIcaoDownloadRunning) {
|
|
let req = regIcaoDownload({ upper: `${upper}` });
|
|
req.done(function() {
|
|
if (regCache[this.opts.upper]) {
|
|
selectPlaneByHex(regCache[this.opts.upper].toLowerCase(), {noDeselect: true, follow: true});
|
|
}
|
|
});
|
|
}
|
|
}
|
|
for (let i in PlanesOrdered) {
|
|
const plane = PlanesOrdered[i];
|
|
if (
|
|
(byCallsign && plane.flight != null && plane.flight.toLowerCase().match(query))
|
|
|| (byIcao && plane.icao.toLowerCase().match(query))
|
|
|| (byReg && plane.registration != null && plane.registration.toLowerCase().match(query))
|
|
|| (byType && plane.icaoType != null && plane.icaoType.toLowerCase().match(query))
|
|
) {
|
|
if (globeIndex) {
|
|
if (plane.inView)
|
|
results.push(plane);
|
|
} else {
|
|
if (plane.checkVisible())
|
|
results.push(plane);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (results.length > 1) {
|
|
toggleMultiSelect("on");
|
|
for (let i in results) {
|
|
select(results[i], {});
|
|
results[i].updateTick(true);
|
|
sp = SelectedPlane = null;
|
|
}
|
|
showWarnings && hideSearchWarning();
|
|
} else if (results.length == 1) {
|
|
selectPlaneByHex(results[0].icao, {noDeselect: true, follow: true});
|
|
console.log("query selected: " + queries);
|
|
showWarnings && hideSearchWarning();
|
|
} else {
|
|
console.log("No match found for query: " + queries);
|
|
let foundByHex = 0;
|
|
if (globeIndex) {
|
|
for (let i in queries) {
|
|
const query = queries[i];
|
|
if (query.toLowerCase().match(/~?[a-f,0-9]{6}/)) {
|
|
console.log("maybe it's an icao, let's try to fetch the history for it!");
|
|
selectPlaneByHex(query, {noDeselect: true, follow: true}) && foundByHex++
|
|
}
|
|
}
|
|
}
|
|
if (foundByHex === 0 && showWarnings) {
|
|
if (globeIndex) {
|
|
showSearchWarning("No match found in current view: " + queries);
|
|
} else {
|
|
showSearchWarning("No match found for query: " + queries);
|
|
}
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
function trailReaper() {
|
|
for (let i in PlanesOrdered) {
|
|
PlanesOrdered[i].reapTrail();
|
|
}
|
|
}
|
|
|
|
function setIndexDistance(index, center, coords) {
|
|
if (index >= 1000) {
|
|
globeIndexDist[index] = ol.sphere.getDistance(center, coords);
|
|
return;
|
|
}
|
|
let tile = globeIndexSpecialTiles[index];
|
|
let min = ol.sphere.getDistance(center, [tile[1], tile[0]]);
|
|
min = Math.min(min, ol.sphere.getDistance(center, [tile[1], tile[2]]));
|
|
min = Math.min(min, ol.sphere.getDistance(center, [tile[3], tile[0]]));
|
|
min = Math.min(min, ol.sphere.getDistance(center, [tile[3], tile[2]]));
|
|
globeIndexDist[index] = min;
|
|
}
|
|
|
|
function getViewOversize(factor) {
|
|
factor || (factor = 1);
|
|
let mapSize = OLMap.getSize();
|
|
let size = [mapSize[0] * factor, mapSize[1] * factor];
|
|
return myExtent(OLMap.getView().calculateExtent(size));
|
|
}
|
|
|
|
function globeIndexes() {
|
|
const center = ol.proj.toLonLat(OLMap.getView().getCenter());
|
|
if (mapIsVisible || lastGlobeExtent == null) {
|
|
lastGlobeExtent = getViewOversize(1.02);
|
|
}
|
|
let extent = lastGlobeExtent.extent;
|
|
const bottomLeft = ol.proj.toLonLat([extent[0], extent[1]]);
|
|
const topRight = ol.proj.toLonLat([extent[2], extent[3]]);
|
|
let x1 = bottomLeft[0];
|
|
let y1 = bottomLeft[1];
|
|
let x2 = topRight[0];
|
|
let y2 = topRight[1];
|
|
if (Math.abs(extent[2] - extent[0]) > 40075016) {
|
|
// all longtitudes in view, only check latitude
|
|
x1 = -179;
|
|
x2 = 179;
|
|
}
|
|
if (y1 < -89.5)
|
|
y1 = -89.5;
|
|
if (y2 > 89.5)
|
|
y2 = 89.5;
|
|
let indexes = [];
|
|
//console.log(x1 + ' ' + x2);
|
|
let grid = globeIndexGrid;
|
|
|
|
let x3 = x1 < x2 ? x2 : 199;
|
|
let count = 0;
|
|
|
|
//console.time('indexes');
|
|
for (let lon = x1; lon < x3 + grid; lon += grid) {
|
|
if (x1 > x2 && lon > 180) {
|
|
lon -= 360;
|
|
x3 = x2;
|
|
}
|
|
if (lon > x3)
|
|
lon = x3 + 0.01;
|
|
if (count++ > 360 / grid) {
|
|
console.log("globeIndexes fail, lon: " + lon);
|
|
}
|
|
let count2 = 0;
|
|
for (let lat = y1; lat < y2 + grid; lat += grid) {
|
|
if (count2++ > 180 / grid) {
|
|
console.log("globeIndexes fail, lon: " + lon + ", lat: " + lat);
|
|
break;
|
|
}
|
|
if (lat > y2)
|
|
lat = y2 + 0.01;
|
|
if (lat > 90)
|
|
break;
|
|
let index = globe_index(lat, lon);
|
|
//console.log(lat + ' ' + lon + ' ' + index);
|
|
if (!indexes.includes(index)) {
|
|
setIndexDistance(index, center, [lon, lat]);
|
|
indexes.push(index);
|
|
}
|
|
}
|
|
}
|
|
//console.timeEnd('indexes');
|
|
globeTilesViewCount = indexes.length;
|
|
return indexes;
|
|
}
|
|
|
|
function globe_index(lat, lon) {
|
|
let grid = globeIndexGrid;
|
|
|
|
lat = grid * Math.floor((lat + 90) / grid) - 90;
|
|
lon = grid * Math.floor((lon + 180) / grid) - 180;
|
|
|
|
let i = Math.floor((lat+90) / grid);
|
|
let j = Math.floor((lon+180) / grid);
|
|
|
|
let lat_multiplier = Math.floor(360 / grid + 1);
|
|
let defaultIndex = i * lat_multiplier + j + 1000;
|
|
|
|
let index = globeIndexSpecialLookup[defaultIndex];
|
|
if (index) {
|
|
return index;
|
|
}
|
|
|
|
// not yet in lookup, check special tiles
|
|
for (let i = 0; i < globeIndexSpecialTiles.length; i++) {
|
|
let tile = globeIndexSpecialTiles[i];
|
|
if ((lat >= tile[0] && lat < tile[2])
|
|
&& ((tile[1] < tile[3] && lon >= tile[1] && lon < tile[3])
|
|
|| (tile[1] > tile[3] && (lon >= tile[1] || lon < tile[3])))) {
|
|
globeIndexSpecialLookup[defaultIndex] = index = i;
|
|
}
|
|
}
|
|
if (index == null) {
|
|
// not a special tile, set lookup to default index
|
|
globeIndexSpecialLookup[defaultIndex] = index = defaultIndex;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
function myExtent(extent) {
|
|
let bottomLeft = ol.proj.toLonLat([extent[0], extent[1]]);
|
|
let topRight = ol.proj.toLonLat([extent[2], extent[3]]);
|
|
return {
|
|
extent: extent,
|
|
minLon: bottomLeft[0],
|
|
maxLon: topRight[0],
|
|
minLat: bottomLeft[1],
|
|
maxLat: topRight[1],
|
|
}
|
|
}
|
|
|
|
function inView(pos, ex) {
|
|
if (pos == null)
|
|
return false;
|
|
|
|
if (solidT)
|
|
return true;
|
|
|
|
let extent = ex.extent;
|
|
let lon = pos[0];
|
|
let lat = pos[1];
|
|
|
|
//console.log((currExtent[2]-currExtent[0])/40075016);
|
|
//console.log([bottomLeft[0], topRight[0]]);
|
|
//console.log([bottomLeft[1], topRight[1]]);
|
|
//const proj = ol.proj.fromLonLat(pos);
|
|
if (lat < ex.minLat || lat > ex.maxLat)
|
|
return false;
|
|
|
|
if (extent[2] - extent[0] > 40075016) {
|
|
// all longtitudes in view, only check latitude
|
|
return true;
|
|
} else if (ex.minLon < ex.maxLon) {
|
|
// no wraparound: view not crossing 179 to -180 transition line
|
|
return (lon > ex.minLon && lon < ex.maxLon);
|
|
} else {
|
|
// wraparound: view crossing 179 to -180 transition line
|
|
return (lon > ex.minLon || lon < ex.maxLon);
|
|
}
|
|
}
|
|
let lastAddressBarUpdate = 0;
|
|
let updateAddressBarTimeout;
|
|
let updateAddressBarPushed = false;
|
|
function updateAddressBar() {
|
|
if (!window.history || !window.history.replaceState)
|
|
return;
|
|
if (heatmap || pTracks || !CenterLat || uuid)
|
|
return;
|
|
|
|
let string = '';
|
|
|
|
if (SelPlanes.length > 0) {
|
|
string += '?icao=' + SelPlanes.map((s) => encodeURIComponent(s.icao)).join(',')
|
|
} else if (replay) {
|
|
string += '?replay=';
|
|
string += zDateString(replay.ts);
|
|
string += '-' + replay.ts.getUTCHours().toString().padStart(2,'0');
|
|
string += ':' + replay.ts.getUTCMinutes().toString().padStart(2,'0');
|
|
}
|
|
|
|
if (showTrace || replay) {
|
|
string += (string ? '&' : '?');
|
|
string += 'lat=' + CenterLat.toFixed(3) + '&lon=' + CenterLon.toFixed(3) + '&zoom=' + zoomLvl.toFixed(1);
|
|
}
|
|
|
|
if (SelPlanes.length > 0 && (showTrace || replay)) {
|
|
string += (string ? '&' : '?');
|
|
string += 'showTrace=' + traceDateString;
|
|
if (legSel != -1)
|
|
string += '&leg=' + (legSel + 1);
|
|
if (traceOpts.startHours != null) {
|
|
string += '&startTime=';
|
|
string += traceOpts.startHours.toString().padStart(2, '0');
|
|
string += ':' + traceOpts.startMinutes.toString().padStart(2, '0');
|
|
if (traceOpts.startSeconds) {
|
|
string += ':' + traceOpts.startSeconds.toString().padStart(2, '0');
|
|
}
|
|
}
|
|
if (traceOpts.endHours != null) {
|
|
string += '&endTime=';
|
|
string += traceOpts.endHours.toString().padStart(2, '0');
|
|
string += ':' + traceOpts.endMinutes.toString().padStart(2, '0');
|
|
if (traceOpts.endSeconds) {
|
|
string += ':' + traceOpts.endSeconds.toString().padStart(2, '0');
|
|
}
|
|
}
|
|
if (trackLabels) {
|
|
string += '&trackLabels';
|
|
}
|
|
if (traceOpts.showTime) {
|
|
string += '×tamp=';
|
|
string += Math.ceil(traceOpts.showTime);
|
|
}
|
|
}
|
|
|
|
let shareFilter = '';
|
|
if (shareFiltersParam || (toggles.shareFilters && toggles.shareFilters.state)) {
|
|
let filterStrings = [];
|
|
|
|
if (PlaneFilter.minAltitude > -1000000) {
|
|
filterStrings.push('filterAltMin=' + PlaneFilter.minAltitude);
|
|
}
|
|
if (PlaneFilter.maxAltitude < 1000000) {
|
|
filterStrings.push('filterAltMax=' + PlaneFilter.maxAltitude);
|
|
}
|
|
if (PlaneFilter.callsign) {
|
|
filterStrings.push('filterCallSign=' + encodeURIComponent(PlaneFilter.callsign));
|
|
}
|
|
if (PlaneFilter.type) {
|
|
filterStrings.push('filterType=' + encodeURIComponent(PlaneFilter.type));
|
|
}
|
|
if (PlaneFilter.description) {
|
|
filterStrings.push('filterDescription=' + encodeURIComponent(PlaneFilter.description));
|
|
}
|
|
if (PlaneFilter.icao) {
|
|
filterStrings.push('filterIcao=' + encodeURIComponent(PlaneFilter.icao));
|
|
}
|
|
|
|
if (PlaneFilter.sources) {
|
|
filterStrings.push('filterSources=' + PlaneFilter.sources.map(f => encodeURIComponent(f)).join(','));
|
|
}
|
|
if (PlaneFilter.flagFilter) {
|
|
filterStrings.push('filterDbFlag=' + PlaneFilter.flagFilter.map(f => encodeURIComponent(f)).join(','));
|
|
}
|
|
|
|
if (filterStrings.length > 0) {
|
|
shareFilter = shareFilter + filterStrings.join('&');
|
|
} else {
|
|
shareFilter = '';
|
|
}
|
|
|
|
//console.log(shareFilter);
|
|
|
|
if (shareFilter) {
|
|
string += (string ? '&' : '?');
|
|
string += shareFilter;
|
|
}
|
|
}
|
|
|
|
if (icaoFilter && !showTrace) {
|
|
string += (string ? '&' : '?');
|
|
string += 'icaoFilter=' + icaoFilter.join(',')
|
|
}
|
|
|
|
if (shareBaseUrl) {
|
|
shareLink = shareBaseUrl + string;
|
|
} else {
|
|
shareLink = window.location.origin + pathName + string;
|
|
}
|
|
//console.log(shareLink);
|
|
|
|
if (!string && !usp.has('showTrace') && !usp.has('icao')) {
|
|
string = initialURL;
|
|
} else {
|
|
string = pathName + string;
|
|
}
|
|
|
|
// Update URL bar
|
|
/*
|
|
let time = new Date().getTime();
|
|
if (time < lastAddressBarUpdate + 200) {
|
|
clearTimeout(updateAddressBarTimeout);
|
|
updateAddressBarTimeout = setTimeout(updateAddressBar, 205);
|
|
return;
|
|
}
|
|
|
|
lastAddressBarUpdate = time;
|
|
*/
|
|
|
|
if (!updateAddressBarPushed) {
|
|
// make sure we keep the thing we clicked on first in the browser history
|
|
window.history.pushState("object or string", "Title", string);
|
|
updateAddressBarPushed = true;
|
|
} else {
|
|
// but don't create a new history entry for every plane we click on
|
|
window.history.replaceState("object or string", "Title", string);
|
|
}
|
|
}
|
|
|
|
function refreshInt() {
|
|
let refresh = RefreshInterval;
|
|
|
|
if (uuid)
|
|
return 5050;
|
|
|
|
// handle non globe case
|
|
if (!globeIndex) {
|
|
return refresh;
|
|
}
|
|
|
|
// handle globe case
|
|
|
|
if (reApi && (binCraft || zstd)) {
|
|
refresh = RefreshInterval * lastRequestSize / 35000;
|
|
let extent = getViewOversize(1.03);
|
|
let min = 0.7;
|
|
let max = 7;
|
|
if (zoomLvl < 5) {
|
|
min += Math.min(1, (5 - zoomLvl) / 4);
|
|
}
|
|
if (refresh < RefreshInterval * min) {
|
|
refresh = RefreshInterval * min;
|
|
}
|
|
if (refresh > RefreshInterval * max) {
|
|
refresh = RefreshInterval * max;
|
|
}
|
|
if (onlySelected && SelPlanes.length == 0 && reApi) {
|
|
// no aircraft selected, none shown
|
|
refresh = RefreshInterval * max * 2;
|
|
}
|
|
if (!FollowSelected && lastRequestBox != requestBoxString()) {
|
|
refresh = Math.min(RefreshInterval, refresh / 4);
|
|
}
|
|
}
|
|
|
|
if (!reApi && binCraft && globeIndex && onlyMilitary && OLMap.getView().getZoom() < 5.5) {
|
|
refresh = 5000;
|
|
}
|
|
|
|
let inactive = getInactive();
|
|
|
|
if (inactive < 70)
|
|
inactive = 70;
|
|
if (inactive > 240)
|
|
inactive = 240;
|
|
|
|
if (globeIndex) {
|
|
refresh *= inactive / 70;
|
|
}
|
|
|
|
if (!mapIsVisible)
|
|
refresh *= 2;
|
|
|
|
if (adsbexchange && window.self != window.top) {
|
|
refresh *= 1.5;
|
|
} else if (onMobile && TrackedAircraftPositions > 800) {
|
|
refresh *= 1.5;
|
|
}
|
|
|
|
//console.log(refresh);
|
|
|
|
return refresh;
|
|
}
|
|
|
|
function toggleShowTrace() {
|
|
if (!showTrace) {
|
|
showTrace = true;
|
|
toggleFollow(false);
|
|
showTraceWasIsolation = onlySelected;
|
|
toggleIsolation("on");
|
|
shiftTrace();
|
|
} else {
|
|
showTrace = false;
|
|
traceOpts = {};
|
|
fetchData();
|
|
legSel = -1;
|
|
jQuery('#leg_sel').text('Legs: All');
|
|
if (!showTraceWasIsolation)
|
|
toggleIsolation("off");
|
|
//let string = pathName + '?icao=' + SelectedPlane.icao;
|
|
//window.history.replaceState("object or string", "Title", string);
|
|
//shareLink = string;
|
|
updateAddressBar();
|
|
const hex = SelectedPlane.icao;
|
|
sp = SelectedPlane = null;
|
|
showTraceExit = true;
|
|
for (let i in SelPlanes) {
|
|
const plane = SelPlanes[i];
|
|
plane.setNull();
|
|
}
|
|
selectPlaneByHex(hex, {noDeselect: true, follow: true, zoom: zoomLvl,});
|
|
}
|
|
|
|
jQuery('#history_collapse').toggle();
|
|
jQuery('#show_trace').toggleClass('active');
|
|
}
|
|
|
|
function legShift(offset, plane) {
|
|
if(!offset)
|
|
offset = 0;
|
|
if (!plane) {
|
|
legSel += offset;
|
|
for (let i in SelPlanes) {
|
|
legShift(offset, SelPlanes[i]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
|
|
if (offset != 0)
|
|
traceOpts.showTime = null;
|
|
|
|
if (!multiSelect && !plane.fullTrace) {
|
|
jQuery('#leg_sel').text('No Data available for\n' + traceDateString);
|
|
jQuery('#trace_time').text('UTC:\n');
|
|
}
|
|
if (!plane.fullTrace) {
|
|
plane.processTrace();
|
|
return;
|
|
}
|
|
|
|
let trace = plane.fullTrace.trace;
|
|
let legStart = null;
|
|
let legEnd = null;
|
|
let count = 0;
|
|
|
|
for (let i = 1; i < trace.length; i++) {
|
|
let timestamp = trace[i][0];
|
|
if (traceOpts.startStamp != null && timestamp < traceOpts.startStamp) {
|
|
continue;
|
|
}
|
|
if (traceOpts.endStamp != null && timestamp > traceOpts.endStamp)
|
|
break;
|
|
if (legStart == null) {
|
|
legStart = i;
|
|
i++;
|
|
if (i >= trace.length)
|
|
break;
|
|
}
|
|
if (trace[i][6] & 2) {
|
|
count++;
|
|
}
|
|
}
|
|
if (legSel < -1)
|
|
legSel = count;
|
|
if (legSel > count)
|
|
legSel = -1;
|
|
|
|
if (legSel == -1) {
|
|
jQuery('#leg_sel').text('Legs: All');
|
|
traceOpts.legStart = null;
|
|
traceOpts.legEnd = null;
|
|
plane.processTrace();
|
|
updateAddressBar();
|
|
return;
|
|
}
|
|
|
|
count = 0;
|
|
for (let i = legStart + 1; i < trace.length; i++) {
|
|
let timestamp = trace[i][0];
|
|
if (traceOpts.endStamp != null && timestamp > traceOpts.endStamp)
|
|
break;
|
|
if (trace[i][6] & 2) {
|
|
if (count == legSel - 1)
|
|
legStart = i;
|
|
if (count == legSel)
|
|
legEnd = i; // exclusive
|
|
count++;
|
|
}
|
|
}
|
|
jQuery('#leg_sel').text('Leg: ' + (legSel + 1));
|
|
traceOpts.legStart = legStart;
|
|
traceOpts.legEnd = legEnd;
|
|
plane.processTrace();
|
|
|
|
updateAddressBar();
|
|
}
|
|
|
|
function setTraceDate(options) {
|
|
options = options || {};
|
|
let numbers = options.string ? options.string.split('-') : [];
|
|
if (numbers.length == 3) {
|
|
traceDate = new Date();
|
|
traceDate.setUTCFullYear(numbers[0]);
|
|
traceDate.setUTCMonth(numbers[1] - 1, numbers[2]);
|
|
} else if (options.ts) {
|
|
traceDate = new Date(options.ts);
|
|
} else {
|
|
return null;
|
|
}
|
|
traceDate.setUTCHours(0);
|
|
traceDate.setUTCMinutes(0);
|
|
traceDate.setUTCSeconds(0);
|
|
|
|
let tomorrow = (new Date()).getTime() + 86400e3;
|
|
if (traceDate.getTime() > tomorrow) {
|
|
traceDate = new Date(tomorrow);
|
|
}
|
|
|
|
traceDateString = zDateString(traceDate);
|
|
|
|
return traceDate;
|
|
}
|
|
|
|
function shiftTrace(offset) {
|
|
if (traceRate > 180) {
|
|
jQuery('#leg_sel').text('Slow down! ...');
|
|
return;
|
|
}
|
|
|
|
// reset some traceOpts stuff (important)
|
|
traceOpts.startStamp = null;
|
|
traceOpts.endStamp = null;
|
|
traceOpts.showTimeEnd = null;
|
|
traceOpts.showTime = null;
|
|
|
|
jQuery('#leg_sel').text('Loading ...');
|
|
if (!traceDate || offset == "today") {
|
|
setTraceDate({ ts: new Date().getTime() });
|
|
} else if (offset) {
|
|
setTraceDate({ ts: traceDate.getTime() + offset * 86400 * 1000 });
|
|
}
|
|
|
|
//jQuery('#trace_date').text('UTC day:\n' + traceDateString);
|
|
jQuery("#histDatePicker").datepicker('setDate', traceDateString);
|
|
|
|
for (let i in SelPlanes) {
|
|
selectPlaneByHex(SelPlanes[i].icao, {noDeselect: true, zoom: zoomLvl});
|
|
}
|
|
|
|
updateAddressBar();
|
|
}
|
|
|
|
|
|
function setLineWidth() {
|
|
newWidth = lineWidth * Math.pow(2, globalScale) / 2 * globalScale
|
|
|
|
estimateStyle = new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: '#808080',
|
|
width: 1.2 * newWidth,
|
|
})
|
|
});
|
|
estimateStyleSlim = new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: '#808080',
|
|
width: 0.4 * newWidth,
|
|
})
|
|
});
|
|
|
|
badLine = new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: '#FF0000',
|
|
width: 2 * newWidth,
|
|
})
|
|
});
|
|
badLineMlat = new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: '#FFA500',
|
|
width: 2 * newWidth,
|
|
})
|
|
});
|
|
|
|
badDot = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: 3.5 * newWidth,
|
|
fill: new ol.style.Fill({
|
|
color: '#FF0000',
|
|
})
|
|
}),
|
|
});
|
|
badDotMlat = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: 3.5 * newWidth,
|
|
fill: new ol.style.Fill({
|
|
color: '#FFA500',
|
|
})
|
|
}),
|
|
});
|
|
|
|
labelFill = new ol.style.Fill({color: 'white' });
|
|
blackFill = new ol.style.Fill({color: 'black' });
|
|
labelStroke = new ol.style.Stroke({color: 'rgba(0,0,0,0.7', width: 4 * globalScale});
|
|
labelStrokeNarrow = new ol.style.Stroke({color: 'rgba(0,0,0,0.7', width: 2.5 * globalScale});
|
|
bgFill = new ol.style.Stroke({color: 'rgba(0,0,0,0.25'});
|
|
}
|
|
let lastCallLocationChange = 0;
|
|
function onLocationChange(position) {
|
|
lastCallLocationChange = new Date().getTime();
|
|
changeCenter();
|
|
const moveMap = (Math.abs(SiteLat - CenterLat) < 0.000001 && Math.abs(SiteLon - CenterLon) < 0.000001);
|
|
SiteLat = DefaultCenterLat = position.coords.latitude;
|
|
SiteLon = DefaultCenterLon = position.coords.longitude;
|
|
SitePosition = [SiteLon, SiteLat];
|
|
|
|
drawSiteCircle();
|
|
createLocationDot();
|
|
|
|
if (moveMap || lockDotCentered) {
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([SiteLon, SiteLat]));
|
|
}
|
|
console.log('Changed Site Location to: '+ SiteLat +', ' + SiteLon);
|
|
//followRandomPlane();
|
|
//togglePersistence();
|
|
}
|
|
function logArg(error) {
|
|
console.log(error);
|
|
}
|
|
|
|
let watchPositionId;
|
|
let pollPositionSeconds = 10;
|
|
function pollPositionInterval() {
|
|
if (!updateLocation || !geoFindEnabled()) {
|
|
return;
|
|
}
|
|
// interval position polling every half minute for browsers that are shit
|
|
//console.trace();
|
|
clearInterval(timers.pollPosition);
|
|
timers.pollPosition = window.setInterval(function() {
|
|
|
|
// if we recently got a new location via watchPosition(), don't query
|
|
if (new Date().getTime() - lastCallLocationChange < pollPositionSeconds * 0.85 * 1000)
|
|
return;
|
|
|
|
if (tabHidden)
|
|
return;
|
|
|
|
console.log('pollPositionInterval: querying position');
|
|
const geoposOptions = {
|
|
enableHighAccuracy: false,
|
|
timeout: pollPositionSeconds * 1000,
|
|
maximumAge: pollPositionSeconds * 1000 ,
|
|
};
|
|
navigator.geolocation.getCurrentPosition(function(position) {
|
|
onLocationChange(position);
|
|
}, logArg, geoposOptions);
|
|
}, pollPositionSeconds * 1000);
|
|
}
|
|
|
|
function watchPosition() {
|
|
if (watchPositionId != null) {
|
|
navigator.geolocation.clearWatch(watchPositionId);
|
|
}
|
|
if (!updateLocation || !geoFindEnabled()) {
|
|
return;
|
|
}
|
|
const geoposOptions = {
|
|
enableHighAccuracy: false,
|
|
timeout: Infinity,
|
|
maximumAge: 25 * 1000,
|
|
};
|
|
watchPositionId = navigator.geolocation.watchPosition(function(position) {
|
|
onLocationChange(position);
|
|
pollPositionSeconds = 60;
|
|
}, logArg, geoposOptions);
|
|
pollPositionInterval();
|
|
}
|
|
|
|
let geoFindInterval = null;
|
|
function geoFindMe() {
|
|
if (!geoFindEnabled()) {
|
|
initSitePos();
|
|
return;
|
|
}
|
|
|
|
function success(position) {
|
|
SiteLat = DefaultCenterLat = position.coords.latitude;
|
|
SiteLon = DefaultCenterLon = position.coords.longitude;
|
|
if (loStore['geoFindMeFirstVisit'] != 'no' && !(usp.has("lat") && usp.has("lon"))) {
|
|
OLMap.getView().setCenter(ol.proj.fromLonLat([SiteLon, SiteLat]));
|
|
loStore['geoFindMeFirstVisit'] = 'no';
|
|
siteCircleLayer.setVisible(true);
|
|
}
|
|
|
|
initSitePos();
|
|
console.log('Location from browser: '+ SiteLat +', ' + SiteLon);
|
|
|
|
|
|
{
|
|
// always update user location every 15 minutes
|
|
clearInterval(geoFindInterval);
|
|
geoFindInterval = window.setInterval(function() {
|
|
if (tabHidden)
|
|
return;
|
|
const geoposOptions = {
|
|
enableHighAccuracy: false,
|
|
timeout: 15 * 60 * 1000,
|
|
maximumAge: 5 * 60 * 1000 ,
|
|
};
|
|
navigator.geolocation.getCurrentPosition(onLocationChange, logArg, geoposOptions);
|
|
}, 15 * 60 * 1000);
|
|
}
|
|
}
|
|
|
|
function error() {
|
|
console.log("Unable to query location.");
|
|
initSitePos();
|
|
}
|
|
|
|
if (!navigator.geolocation) {
|
|
console.log('Geolocation is not supported by your browser');
|
|
} else {
|
|
// change SitePos on location change
|
|
console.log('Locating…');
|
|
const geoposOptions = {
|
|
enableHighAccuracy: false,
|
|
timeout: Infinity,
|
|
maximumAge: 300 * 1000,
|
|
};
|
|
navigator.geolocation.getCurrentPosition(success, error, geoposOptions);
|
|
}
|
|
}
|
|
|
|
window.mobilecheck = function() {
|
|
let check = false;
|
|
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4))) check = true;})(navigator.userAgent||navigator.vendor||window.opera);
|
|
return check;
|
|
};
|
|
|
|
function initSitePos() {
|
|
let lastSitePosition = SitePosition;
|
|
// Set SitePosition
|
|
if (SiteLat != null && SiteLon != null) {
|
|
SitePosition = [SiteLon, SiteLat];
|
|
// Add home marker if requested
|
|
drawSiteCircle();
|
|
createLocationDot();
|
|
} else {
|
|
TAR.planeMan.setColumnVis('distance', false);
|
|
}
|
|
|
|
if (!lastSitePosition) {
|
|
if (SitePosition) {
|
|
TAR.planeMan.cols.distance.sort();
|
|
} else {
|
|
TAR.planeMan.cols.altitude.sort();
|
|
TAR.planeMan.cols.altitude.sort();
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
function drawAlt() {
|
|
processAircraft({hex: 'c0ffee', });
|
|
let plane = Planes['c0ffee'];
|
|
newWidth = 4;
|
|
for (let i = 0; i <= 50000; i += 500) {
|
|
plane.position = [i/10000, 0];
|
|
plane.altitude = i;
|
|
plane.alt_rounded = calcAltitudeRounded(plane.altitude);
|
|
plane.updateTrack(now - i, now - i - 5000, { serverTrack: true });
|
|
}
|
|
}
|
|
*/
|
|
|
|
function remakeTrails() {
|
|
for (let i in PlanesOrdered) {
|
|
PlanesOrdered[i].remakeTrail();
|
|
PlanesOrdered[i].updateFeatures(true);
|
|
}
|
|
}
|
|
|
|
function createLocationDot() {
|
|
locationDotFeatures.clear();
|
|
let markerStyle = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: 7,
|
|
snapToPixel: false,
|
|
fill: new ol.style.Fill({color: 'black'}),
|
|
stroke: new ol.style.Stroke({
|
|
color: 'white', width: 2
|
|
})
|
|
})
|
|
});
|
|
|
|
let feature = new ol.Feature(new ol.geom.Point(ol.proj.fromLonLat(SitePosition)));
|
|
feature.setStyle(markerStyle);
|
|
locationDotFeatures.addFeature(feature);
|
|
}
|
|
function drawSiteCircle() {
|
|
siteCircleFeatures.clear();
|
|
|
|
if (!SitePosition)
|
|
return;
|
|
|
|
let circleColor = '#000000';
|
|
|
|
for (let i = 0; i < SiteCirclesDistances.length; i++) {
|
|
circleColor = i < SiteCirclesColors.length ? SiteCirclesColors[i] : circleColor;
|
|
|
|
let conversionFactor = 1000.0;
|
|
if (DisplayUnits === "nautical") {
|
|
conversionFactor = 1852.0;
|
|
} else if (DisplayUnits === "imperial") {
|
|
conversionFactor = 1609.0;
|
|
}
|
|
|
|
let distance = SiteCirclesDistances[i] * conversionFactor;
|
|
let circle = TAR.utils.make_geodesic_circle(SitePosition, distance, 180);
|
|
circle.transform('EPSG:4326', 'EPSG:3857');
|
|
let feature = new ol.Feature(circle);
|
|
|
|
let circleStyle = new ol.style.Style({
|
|
fill: null,
|
|
stroke: new ol.style.Stroke({
|
|
color: circleColor,
|
|
lineDash: SiteCirclesLineDash,
|
|
width: globalScale,
|
|
}),
|
|
text: new ol.style.Text({
|
|
font: ((10 * globalScale) + 'px Helvetica Neue, Helvetica, Tahoma, Verdana, sans-serif'),
|
|
fill: new ol.style.Fill({ color: '#000' }),
|
|
offsetY: -8,
|
|
text: format_distance_long(distance, DisplayUnits, 0),
|
|
})
|
|
});
|
|
|
|
feature.setStyle(circleStyle);
|
|
siteCircleFeatures.addFeature(feature);
|
|
}
|
|
}
|
|
|
|
let calcOutlineFeatures = new ol.source.Vector();
|
|
let calcOutlineLayer;
|
|
function drawUpintheair() {
|
|
// Add terrain-limit rings. To enable this:
|
|
//
|
|
// create a panorama for your receiver location on heywhatsthat.com
|
|
//
|
|
// note the "view" value from the URL at the top of the panorama
|
|
// i.e. the XXXX in http://www.heywhatsthat.com/?view=XXXX
|
|
//
|
|
// fetch a json file from the API for the altitudes you want to see:
|
|
//
|
|
// wget -O /usr/local/share/tar1090/html/upintheair.json \
|
|
// 'http://www.heywhatsthat.com/api/upintheair.json?id=XXXX&refraction=0.25&alts=3048,9144'
|
|
//
|
|
// NB: altitudes are in _meters_, you can specify a list of altitudes
|
|
//
|
|
if (!calcOutlineData)
|
|
return;
|
|
|
|
let data = calcOutlineData;
|
|
for (let i = 0; i < data.rings.length; ++i) {
|
|
let geom = null;
|
|
let points = data.rings[i].points;
|
|
let altitude = (3.28084 * data.rings[i].alt).toFixed(0);
|
|
let color = range_outline_color;
|
|
if (range_outline_colored_by_altitude) {
|
|
let colorArr = altitudeColor(altitude);
|
|
color = 'hsl(' + colorArr[0].toFixed(0) + ',' + colorArr[1].toFixed(0) + '%,' + colorArr[2].toFixed(0) + '%)';
|
|
}
|
|
let outlineStyle = new ol.style.Style({
|
|
fill: null,
|
|
stroke: new ol.style.Stroke({
|
|
color: color,
|
|
width: range_outline_width,
|
|
lineDash: range_outline_dash,
|
|
})
|
|
});
|
|
if (points.length > 0) {
|
|
geom = new ol.geom.LineString([[ points[0][1], points[0][0] ]]);
|
|
for (let j = 0; j < points.length; ++j) {
|
|
geom.appendCoordinate([ points[j][1], points[j][0] ]);
|
|
}
|
|
geom.appendCoordinate([ points[0][1], points[0][0] ]);
|
|
geom.transform('EPSG:4326', 'EPSG:3857');
|
|
|
|
let feature = new ol.Feature(geom);
|
|
feature.setStyle(outlineStyle);
|
|
calcOutlineFeatures.addFeature(feature);
|
|
}
|
|
}
|
|
}
|
|
let actualOutlineLayer;
|
|
let actualOutlineFeatures;
|
|
let actualOutlineStyle;
|
|
|
|
function drawOutlineJson() {
|
|
if (!receiverJson || !receiverJson.outlineJson)
|
|
return;
|
|
let request = jQuery.ajax({ url: 'data/outline.json',
|
|
cache: false,
|
|
dataType: 'json' });
|
|
request.done(function(data) {
|
|
actualOutlineFeatures.clear();
|
|
let points;
|
|
if (data.actualRange && data.actualRange.last24h) {
|
|
points = data.actualRange.last24h.points;
|
|
} else {
|
|
points = data.points;
|
|
}
|
|
if (!points || !points.length)
|
|
return;
|
|
let geom = null;
|
|
let lastLon = null;
|
|
for (let j = 0; j < points.length + 1; ++j) {
|
|
const k = j % points.length;
|
|
const lat = points[k][0];
|
|
const lon = points[k][1];
|
|
const proj = ol.proj.fromLonLat([lon, lat]);
|
|
if (!geom || (lastLon && Math.abs(lon - lastLon) > 270)) {
|
|
geom = new ol.geom.LineString([proj]);
|
|
actualOutlineFeatures.addFeature(new ol.Feature(geom));
|
|
} else {
|
|
geom.appendCoordinate(proj);
|
|
}
|
|
lastLon = lon;
|
|
}
|
|
});
|
|
|
|
request.fail(function() {
|
|
// no rings available, do nothing
|
|
});
|
|
}
|
|
|
|
function gotoTime(timestamp) {
|
|
clearTimeout(traceOpts.showTimeout);
|
|
if (timestamp) {
|
|
traceOpts.showTime = timestamp;
|
|
traceOpts.animate = false;
|
|
}
|
|
if (!traceOpts.animate) {
|
|
legShift(0);
|
|
} else {
|
|
let marker = SelectedPlane.glMarker || SelectedPlane.marker;
|
|
if (marker) {
|
|
|
|
traceOpts.animatePos[0] += (traceOpts.animateToLon - traceOpts.animateFromLon) / traceOpts.animateSteps;
|
|
traceOpts.animatePos[1] += (traceOpts.animateToLat - traceOpts.animateFromLat) / traceOpts.animateSteps;
|
|
|
|
SelectedPlane.updateMarker();
|
|
}
|
|
if (--traceOpts.animateCounter == 1) {
|
|
traceOpts.animate = false;
|
|
traceOpts.showTime = traceOpts.showTimeEnd;
|
|
console.log(traceOpts.showTime);
|
|
}
|
|
|
|
traceOpts.animateStepTime = traceOpts.animateRealtime / traceOpts.replaySpeed / traceOpts.animateSteps;
|
|
traceOpts.showTimeout = setTimeout(gotoTime, traceOpts.animateStepTime);
|
|
}
|
|
}
|
|
|
|
function checkFollow() {
|
|
if (!FollowSelected)
|
|
return false;
|
|
if (!SelectedPlane || !SelectedPlane.position) {
|
|
toggleFollow(false);
|
|
return false;
|
|
}
|
|
const center = OLMap.getView().getCenter();
|
|
let proj = SelectedPlane.proj;
|
|
|
|
if (!proj) {
|
|
return false;
|
|
}
|
|
|
|
if (Math.abs(center[0] - proj[0]) > 1 ||
|
|
Math.abs(center[1] - proj[1]) > 1)
|
|
{
|
|
toggleFollow(false);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function everySecond() {
|
|
if (traceRate > 0)
|
|
traceRate = traceRate * 0.985 - 1;
|
|
updateIconCache();
|
|
}
|
|
|
|
let getTraceTimeout = null;
|
|
function getTrace(newPlane, hex, options) {
|
|
|
|
if (options.list) {
|
|
newPlane = options.list.pop()
|
|
if (!newPlane) {
|
|
return;
|
|
}
|
|
hex = newPlane.icao;
|
|
}
|
|
|
|
if (!newPlane) {
|
|
newPlane = Planes[hex] || new PlaneObject(hex);
|
|
newPlane.last_message_time = NaN;
|
|
newPlane.position_time = NaN;
|
|
select(newPlane, options);
|
|
}
|
|
|
|
let time = new Date().getTime();
|
|
let backoff = 200;
|
|
if (!showTrace && !solidT && traceRate > 140 && time < lastTraceGet + backoff) {
|
|
clearTimeout(getTraceTimeout);
|
|
getTraceTimeout = setTimeout(getTrace, lastTraceGet + backoff + 20 - time, newPlane, hex, options);
|
|
return newPlane;
|
|
}
|
|
|
|
lastTraceGet = time;
|
|
|
|
let URL1;
|
|
let URL2;
|
|
//console.log('Requesting trace: ' + hex);
|
|
|
|
// use non historic traces until 60 min after midnight
|
|
let today = new Date();
|
|
if ((showTrace || replay) && !(today.getTime() > traceDate.getTime() && today.getTime() < traceDate.getTime() + (24 * 3600 + 60 * 60) * 1000)) {
|
|
let dateString = traceDateString || zDateString(today);
|
|
URL1 = null;
|
|
URL2 = 'globe_history/' + dateString.replace(/-/g, '/') + '/traces/' + hex.slice(-2) + '/trace_full_' + hex + '.json';
|
|
traceRate += 3;
|
|
} else {
|
|
URL1 = 'data/traces/'+ hex.slice(-2) + '/trace_recent_' + hex + '.json';
|
|
URL2 = 'data/traces/'+ hex.slice(-2) + '/trace_full_' + hex + '.json';
|
|
traceRate += 2;
|
|
}
|
|
if (showTrace && trace_hist_only) {
|
|
let dateString = traceDateString || zDateString(today);
|
|
URL2 = 'globe_history/' + dateString.replace(/-/g, '/') + '/traces/' + hex.slice(-2) + '/trace_full_' + hex + '.json';
|
|
}
|
|
|
|
traceOpts.follow = (options.follow == true);
|
|
|
|
if (showTrace) {
|
|
//console.log(today.toUTCString() + ' ' + traceDate.toUTCString());
|
|
|
|
if (traceOpts.startHours == null || traceOpts.startHours < 0)
|
|
traceOpts.startStamp = traceDate.getTime() / 1000;
|
|
else
|
|
traceOpts.startStamp = traceDate.getTime() / 1000 + traceOpts.startHours * 3600 + traceOpts.startMinutes * 60 + traceOpts.startSeconds;
|
|
|
|
if (traceOpts.endHours == null || traceOpts.endHours >= 24)
|
|
traceOpts.endStamp = traceDate.getTime() / 1000 + 24 * 3600;
|
|
else
|
|
traceOpts.endStamp = traceDate.getTime() / 1000 + traceOpts.endHours * 3600 + traceOpts.endMinutes * 60 + traceOpts.endSeconds;
|
|
}
|
|
|
|
if (newPlane && (showTrace || showTraceExit)) {
|
|
newPlane.trace = [];
|
|
newPlane.recentTrace = null;
|
|
newPlane.fullTrace = null;
|
|
}
|
|
|
|
//console.log(URL2);
|
|
|
|
//options = JSON.parse(JSON.stringify(options));
|
|
options.plane = `${newPlane.icao}`;
|
|
options.defer = jQuery.Deferred();
|
|
|
|
if (URL1 && !options.onlyFull) {
|
|
jQuery.ajax({ url: `${URL1}`,
|
|
dataType: 'json',
|
|
options: options,
|
|
})
|
|
.done(function(data) {
|
|
const options = this.options;
|
|
const plane = Planes[options.plane];
|
|
plane.recentTrace = normalizeTraceStamps(data);
|
|
if (!showTrace) {
|
|
plane.processTrace();
|
|
if (options.follow)
|
|
toggleFollow(true);
|
|
}
|
|
options.defer.resolve(options);
|
|
if (options.onlyRecent && options.list) {
|
|
newPlane.updateLines();
|
|
getTrace(null, null, options);
|
|
}
|
|
this.options = null;
|
|
});
|
|
} else {
|
|
options.defer.resolve(options);
|
|
}
|
|
|
|
if (options.onlyRecent)
|
|
return newPlane;
|
|
|
|
jQuery.ajax({ url: `${URL2}`,
|
|
dataType: 'json',
|
|
options: options,
|
|
})
|
|
.done(function(data) {
|
|
const options = this.options;
|
|
const plane = Planes[options.plane];
|
|
plane.fullTrace = normalizeTraceStamps(data);
|
|
options.defer.done(function(options) {
|
|
const plane = Planes[options.plane];
|
|
if (showTrace) {
|
|
legShift(0, plane);
|
|
if (!multiSelect) {
|
|
gotoTime(showTraceTimestamp);
|
|
}
|
|
} else {
|
|
plane.processTrace();
|
|
if (options.follow)
|
|
toggleFollow(true);
|
|
}
|
|
});
|
|
if (options.list) {
|
|
newPlane.updateLines();
|
|
getTrace(null, null, options);
|
|
}
|
|
options.defer = null;
|
|
this.options = null;
|
|
})
|
|
.fail(function() {
|
|
const options = this.options;
|
|
const plane = Planes[options.plane];
|
|
if (showTrace)
|
|
legShift(0, plane);
|
|
else
|
|
plane.processTrace();
|
|
|
|
if (options.list) {
|
|
getTrace(null, null, options);
|
|
} else {
|
|
plane.getAircraftData();
|
|
refreshSelected();
|
|
}
|
|
this.options = null;
|
|
});
|
|
|
|
return newPlane;
|
|
}
|
|
|
|
function initHeatmap() {
|
|
heatmap.init = false;
|
|
if (heatFeatures.length == 0) {
|
|
for (let i = 0; i < heatFeaturesSpread; i++) {
|
|
heatFeatures.push(new ol.source.Vector());
|
|
heatLayers.push(new ol.layer.Vector({
|
|
name: ('heatLayer' + i),
|
|
isTrail: true,
|
|
source: heatFeatures[i],
|
|
declutter: (heatmap.declutter ? true : false),
|
|
zIndex: 150,
|
|
renderOrder: null,
|
|
renderBuffer: 5,
|
|
}));
|
|
trailGroup.push(heatLayers[i]);
|
|
}
|
|
}
|
|
realHeat = new ol.layer.Heatmap({
|
|
source: realHeatFeatures,
|
|
name: realHeat,
|
|
isTrail: true,
|
|
zIndex: 150,
|
|
weight: x => heatmap.weight,
|
|
radius: heatmap.radius,
|
|
blur: heatmap.blur,
|
|
});
|
|
trailGroup.push(realHeat);
|
|
}
|
|
|
|
function setSize(set) {
|
|
let count = 0;
|
|
for (const i in set.values())
|
|
count++;
|
|
return count;
|
|
}
|
|
|
|
function drawHeatmap() {
|
|
if (!heatmap)
|
|
return;
|
|
if (heatmap.init) {
|
|
initHeatmap();
|
|
}
|
|
|
|
console.time("drawHeat");
|
|
|
|
let ext = myExtent(OLMap.getView().calculateExtent(OLMap.getSize()));
|
|
let maxLat = ext.maxLat * 1000000;
|
|
let minLat = ext.minLat * 1000000;
|
|
|
|
webglFeatures.clear();
|
|
for (let i = 0; i < heatFeaturesSpread; i++)
|
|
heatFeatures[i].clear();
|
|
realHeatFeatures.clear();
|
|
|
|
let pointCount = 0;
|
|
let features = [];
|
|
if (lineStyleCache["scale"] != globalScale) {
|
|
lineStyleCache = {};
|
|
lineStyleCache["scale"] = globalScale;
|
|
}
|
|
let done = new Set();
|
|
let iterations = 0;
|
|
let maxIter = 1000 * 1000;
|
|
|
|
|
|
let tempPoints = [];
|
|
for (let k = 0; k < heatChunks.length; k++) {
|
|
if (heatPoints[k] != null) {
|
|
true; // do nothing
|
|
} else if (heatChunks[k] != null) {
|
|
if (heatChunks[k].byteLength % 16 != 0) {
|
|
console.log("Invalid heatmap file (byteLength): " + k);
|
|
continue;
|
|
}
|
|
let points = heatPoints[k] = new Int32Array(heatChunks[k]);
|
|
let found = 0;
|
|
for (let i = 0; i < points.length; i += 4) {
|
|
if (points[i] == 0xe7f7c9d) {
|
|
found = 1;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
heatPoints[k] = heatChunks[k] = null;
|
|
console.log("Invalid heatmap file (magic number): " + k);
|
|
}
|
|
} else {
|
|
continue;
|
|
}
|
|
tempPoints.push(heatPoints[k]);
|
|
}
|
|
|
|
//console.log('tempPoints.length: ' + tempPoints.length);
|
|
let myPoints = [];
|
|
if (tempPoints.length <= 2) {
|
|
myPoints = tempPoints;
|
|
} else {
|
|
let len = tempPoints.length;
|
|
let arr1 = tempPoints.splice(0, Math.round(tempPoints.length / 3));
|
|
let arr2 = tempPoints.splice(0, Math.round(tempPoints.length / 2));
|
|
let arr3 = tempPoints;
|
|
myPoints.push(arr2.splice(0, 1));
|
|
myPoints.push(arr3.splice(0, 1));
|
|
myPoints.push(arr1.splice(0, 1));
|
|
len -= 3;
|
|
for (let i = 0; i < Math.ceil(len / 3); i++) {
|
|
myPoints.push(arr2.splice(0, 1));
|
|
myPoints.push(arr3.splice(0, 1));
|
|
myPoints.push(arr1.splice(0, 1));
|
|
}
|
|
}
|
|
myPoints = myPoints.flat();
|
|
|
|
//console.log('myPoints.length: ' + myPoints.length);
|
|
|
|
let indexes = [];
|
|
for (let k = 0; k < myPoints.length; k++) {
|
|
let points = myPoints[k];
|
|
let index = [];
|
|
let i = 0;
|
|
if (!points)
|
|
continue;
|
|
while(points[i] != 0xe7f7c9d && i < points.length) {
|
|
index.push(points[i]);
|
|
//console.log(points[i]);
|
|
i += 4;
|
|
}
|
|
if (!heatmap.lines)
|
|
index.sort((a, b) => (Math.random() - 0.5));
|
|
indexes.push(index);
|
|
}
|
|
|
|
let offsets = Array(myPoints.length).fill(0);
|
|
|
|
while (pointCount < heatmap.max && done.size < myPoints.length && iterations++ < maxIter) {
|
|
for (let k = 0; k < myPoints.length && pointCount < heatmap.max; k++) {
|
|
|
|
if (offsets[k] > indexes[k].length) {
|
|
continue;
|
|
}
|
|
if (offsets[k] == indexes[k].length) {
|
|
done.add(k);
|
|
offsets[k]++;
|
|
continue;
|
|
}
|
|
|
|
let points = myPoints[k];
|
|
|
|
let i = 4 * indexes[k][offsets[k]];
|
|
|
|
if (points[i] == 0xe7f7c9d)
|
|
i += 4;
|
|
|
|
if (i < 0) {
|
|
console.log('wat ' + i);
|
|
break;
|
|
}
|
|
for (; i < points.length; i += 4) {
|
|
if (points[i] == 0xe7f7c9d)
|
|
break;
|
|
let lat = points[i+1];
|
|
if (lat > maxLat || lat < minLat)
|
|
continue;
|
|
|
|
lat /= 1000000;
|
|
let lon = points[i + 2] / 1000000;
|
|
let pos = [lon, lat];
|
|
|
|
if (!inView(pos, ext))
|
|
continue;
|
|
|
|
let alt = points[i + 3] & 65535;
|
|
if (alt & 32768)
|
|
alt |= -65536;
|
|
if (alt == -123)
|
|
alt = 'ground';
|
|
else
|
|
alt *= 25;
|
|
|
|
let gs = points[i + 3] >> 16;
|
|
if (gs == -1)
|
|
gs = null;
|
|
else
|
|
gs /= 10;
|
|
|
|
if (PlaneFilter.enabled && altFiltered(alt))
|
|
continue;
|
|
|
|
pointCount++;
|
|
//console.log(pos);
|
|
|
|
alt = calcAltitudeRounded(alt);
|
|
let projHere = ol.proj.fromLonLat(pos);
|
|
let style = lineStyleCache[alt];
|
|
let hsl = altitudeColor(alt);
|
|
hsl[1] = hsl[1] * 0.85;
|
|
hsl[2] = hsl[2] * 0.8;
|
|
if (!style) {
|
|
let col;
|
|
if (heatmap.alpha == null)
|
|
col = hslToRgb(hsl);
|
|
else
|
|
col = hslToRgb(hsl, heatmap.alpha);
|
|
|
|
style = new ol.style.Style({
|
|
image: new ol.style.Circle({
|
|
radius: heatmap.radius * globalScale,
|
|
fill: new ol.style.Fill({
|
|
color: col,
|
|
}),
|
|
}),
|
|
zIndex: i,
|
|
});
|
|
lineStyleCache[alt] = style;
|
|
}
|
|
let feat = new ol.Feature(new ol.geom.Point(projHere));
|
|
if (webgl) {
|
|
let rgb = hslToRgb(hsl, 'array');
|
|
feat.set('r', rgb[0]);
|
|
feat.set('g', rgb[1]);
|
|
feat.set('b', rgb[2]);
|
|
} else {
|
|
feat.setStyle(style);
|
|
}
|
|
features.push(feat);
|
|
//console.log(alt);
|
|
}
|
|
offsets[k] += 1;
|
|
}
|
|
}
|
|
if (iterations >= maxIter)
|
|
console.log("drawHeatmap: MAX_ITERATIONS!");
|
|
//console.log(setSize(done));
|
|
console.log("files: " + myPoints.length + ", points drawn: " + pointCount);
|
|
if (heatmap.real) {
|
|
realHeatFeatures.addFeatures(features);
|
|
} else {
|
|
|
|
if (webgl) {
|
|
webglFeatures.addFeatures(features);
|
|
} else {
|
|
for (let i = 0; i < heatFeaturesSpread; i++) {
|
|
|
|
heatFeatures[i].addFeatures(features.splice(0, pointCount / heatFeaturesSpread + 1));
|
|
//console.log(features.length);
|
|
}
|
|
}
|
|
}
|
|
console.timeEnd("drawHeat");
|
|
jQuery("#loader").addClass("hidden");
|
|
}
|
|
|
|
function currentExtent(factor) {
|
|
let size = OLMap.getSize();
|
|
if (factor != null)
|
|
size = [size[0] * factor, size[1] * factor];
|
|
return myExtent(OLMap.getView().calculateExtent(size));
|
|
}
|
|
|
|
function replayDefaults(ts) {
|
|
jQuery("#replayPlay").html("Pause");
|
|
return {
|
|
playing: true,
|
|
ts: ts,
|
|
ival: 60 * 1000,
|
|
speed: 30,
|
|
dateText: zDateString(ts),
|
|
hours: ts.getUTCHours(),
|
|
minutes: ts.getUTCMinutes(),
|
|
};
|
|
}
|
|
|
|
function replayClear() {
|
|
clearTimeout(refreshId);
|
|
reaper(true);
|
|
refreshFilter();
|
|
}
|
|
|
|
let replayData;
|
|
let replayDataKey;
|
|
function loadReplay(ts) {
|
|
if (isNaN(ts.getTime())) {
|
|
ts = new Date();
|
|
}
|
|
let lastAvailable = new Date();
|
|
lastAvailable.setUTCMinutes(Math.floor(lastAvailable.getUTCMinutes() / 30) * 30);
|
|
lastAvailable.setUTCSeconds(0);
|
|
lastAvailable = lastAvailable.getTime() - 10 * 1000;
|
|
if (ts.getTime() > lastAvailable) {
|
|
ts = new Date(lastAvailable);
|
|
ts.setUTCMinutes(Math.floor(ts.getUTCMinutes() / 30) * 30 + 1);
|
|
ts.setUTCSeconds(0);
|
|
console.log('not available, using this time: ' + ts);
|
|
replayClear();
|
|
}
|
|
replay.ts = ts;
|
|
replaySetTimeHint();
|
|
|
|
let time = new Date(ts);
|
|
let sDate = sDateString(time);
|
|
let index = 2 * time.getUTCHours() + Math.floor(time.getUTCMinutes() / 30);
|
|
|
|
let rKey = sDate + index;
|
|
if (rKey == replayDataKey) {
|
|
initReplay(replayData);
|
|
} else {
|
|
let req = jQuery.ajax({
|
|
url: "globe_history/" + sDate + "/heatmap/" + index.toString().padStart(2, '0') + ".bin.ttf",
|
|
method: 'GET',
|
|
xhr: arraybufferRequest,
|
|
rKey: rKey,
|
|
});
|
|
|
|
setTraceDate({ts: ts});
|
|
|
|
req.done(function(data) {
|
|
if (!data) {
|
|
console.log("initReplay: no data!");
|
|
return;
|
|
}
|
|
replayData = data;
|
|
replayDataKey = this.rKey;
|
|
initReplay(data);
|
|
});
|
|
req.fail(function(jqxhr, status, error) {
|
|
jQuery("#update_error_detail").text(jqxhr.status + ' --> No data for this timestamp!');
|
|
jQuery("#update_error").css('display','block');
|
|
setTimeout(function() {jQuery("#update_error").css('display','none');}, 5000);
|
|
});
|
|
}
|
|
}
|
|
function initReplay(data) {
|
|
if (data.byteLength % 16 != 0) {
|
|
console.log("Invalid heatmap file (byteLength)");
|
|
return;
|
|
}
|
|
let points = new Int32Array(data);
|
|
let pointsU = new Uint32Array(data);
|
|
let pointsU8 = new Uint8Array(data);
|
|
let found = 0;
|
|
replay.slices = [];
|
|
for (let i = 0; i < points.length; i += 4) {
|
|
if (points[i] == 0xe7f7c9d) {
|
|
found = 1;
|
|
replay.slices.push(i);
|
|
}
|
|
}
|
|
if (!found) {
|
|
console.log("Invalid heatmap file (magic number)");
|
|
replay.points = null;
|
|
replay.pointsU = null;
|
|
replay.pointsU8 = null;
|
|
return;
|
|
}
|
|
replay.points = points;
|
|
replay.pointsU = pointsU;
|
|
replay.pointsU8 = pointsU8;
|
|
|
|
refreshFilter();
|
|
|
|
replay.ival = (replay.pointsU[replay.slices[0] + 3] & 65535) / 1000;
|
|
replay.halfHour = (replay.ts.getUTCMinutes() >= 30) ? 1 : 0;
|
|
let index = Math.round (((replay.ts.getUTCMinutes() % 30) * 60 + replay.ts.getUTCSeconds()) / replay.ival);
|
|
//console.log("init with index" + replay.index);
|
|
if (index > 0) {
|
|
if (false && index > 1) {
|
|
replay.index = 0
|
|
replayStep("fast");
|
|
}
|
|
replay.index = index - 1;
|
|
replayStep("fast");
|
|
}
|
|
replay.index = index;
|
|
replayStep();
|
|
}
|
|
|
|
function replayOnSliderMove() {
|
|
clearTimeout(refreshId);
|
|
|
|
let date = new Date(replay.dateText);
|
|
date.setUTCHours(Number(replay.hours));
|
|
date.setUTCMinutes(Number(replay.minutes));
|
|
replay.seconds = 0;
|
|
date.setUTCSeconds(Number(replay.seconds));
|
|
if (true || utcTimesHistoric) {
|
|
jQuery("#replayDateHint").html("Date: " + zDateString(date));
|
|
jQuery("#replayTimeHint").html("Time: " + zuluTime(date) + NBSP + 'Z');
|
|
} else {
|
|
jQuery("#replayDateHint").html("Date: " + lDateString(date));
|
|
jQuery("#replayTimeHint").html("Time: " + localTime(date) + NBSP + TIMEZONE);
|
|
}
|
|
}
|
|
let replayJumpEnabled = true;
|
|
function replayJump() {
|
|
if (!showingReplayBar)
|
|
return;
|
|
if (!replayJumpEnabled)
|
|
return;
|
|
let date = new Date(replay.dateText);
|
|
date.setUTCHours(Number(replay.hours));
|
|
date.setUTCMinutes(Number(replay.minutes));
|
|
date.setUTCSeconds(Number(replay.seconds));
|
|
|
|
let ts = new Date(replay.ts.getTime());
|
|
|
|
// diff less 10 seconds
|
|
if (Math.abs(date.getTime() - ts.getTime()) < 10000) {
|
|
return;
|
|
}
|
|
//console.log(replay.minutes.toString() + ' ' + ts.toString() + ' ' + (date.getTime() - ts.getTime()).toString());
|
|
|
|
//console.trace();
|
|
console.log('jump: ' + date.toUTCString());
|
|
|
|
replayClear();
|
|
loadReplay(date);
|
|
}
|
|
function replaySetTimeHint(arg) {
|
|
replayJumpEnabled = false;
|
|
let dateString;
|
|
let timeString;
|
|
if (true || utcTimesHistoric) {
|
|
dateString = zDateString(replay.ts);
|
|
timeString = zuluTime(replay.ts) + NBSP + 'Z';
|
|
} else {
|
|
dateString = lDateString(replay.ts);
|
|
timeString = localTime(replay.ts) + NBSP + TIMEZONE;
|
|
}
|
|
jQuery("#replayDateHint").html("Date: " + dateString);
|
|
jQuery("#replayTimeHint").html("Time: " + timeString);
|
|
jQuery("#replayDatepicker").datepicker('setDate', dateString);
|
|
|
|
|
|
let hours = replay.ts.getUTCHours();
|
|
jQuery('#hourSelect').slider("option", "value", hours);
|
|
|
|
let minutes = replay.ts.getUTCMinutes();
|
|
jQuery('#minuteSelect').slider("option", "value", minutes);
|
|
replayJumpEnabled = true;
|
|
}
|
|
|
|
function replayStep(arg) {
|
|
if (!replay || showTrace) {
|
|
return;
|
|
}
|
|
|
|
if (replay.playing) {
|
|
clearTimeout(refreshId);
|
|
refreshId = setTimeout(replayStep, replay.ival / replay.speed * 1000);
|
|
}
|
|
|
|
if (isNaN(replay.ts.getTime())) {
|
|
loadReplay(new Date());
|
|
return;
|
|
}
|
|
let index = replay.index;
|
|
if (index >= replay.slices.length) {
|
|
console.log('next half hour');
|
|
let date = new Date(replay.ts.getTime() + 30 * 60 * 1000);
|
|
date.setUTCMinutes(Math.floor(date.getUTCMinutes() / 30) * 30);
|
|
date.setUTCSeconds(0);
|
|
clearTimeout(refreshId);
|
|
loadReplay(date);
|
|
return;
|
|
}
|
|
|
|
let minutes = replay.halfHour * 30 + Math.floor(replay.ival * index / 60);
|
|
let seconds = (replay.ival * index) % 60;
|
|
//console.log(minutes.toString() + ' ' + seconds.toString());
|
|
replay.ts.setUTCMinutes(minutes)
|
|
replay.ts.setUTCSeconds(seconds)
|
|
|
|
replay.hours = replay.ts.getUTCHours();
|
|
replay.minutes = minutes;
|
|
replay.seconds = seconds;
|
|
|
|
let points = replay.points;
|
|
let pointsU = replay.pointsU;
|
|
let i = replay.slices[index];
|
|
|
|
//console.log('index: ' + index + ', i: ' + i);
|
|
|
|
last = now;
|
|
now = replay.pointsU[i + 2] / 1000 + replay.pointsU[i + 1] * 4294967.296;
|
|
|
|
traceOpts.endStamp = now + replay.ival;
|
|
|
|
replay.ival = (replay.pointsU[i + 3] & 65535) / 1000;
|
|
|
|
if (arg != 'fast') {
|
|
replaySetTimeHint();
|
|
updateAddressBar();
|
|
if (index % 5 == 0) {
|
|
console.log(replay.ts.toUTCString());
|
|
reaper();
|
|
}
|
|
}
|
|
|
|
i += 4;
|
|
|
|
let ext = currentExtent(1.4);
|
|
ext.maxLat *= 1e6;
|
|
ext.maxLon *= 1e6;
|
|
ext.minLat *= 1e6;
|
|
ext.minLon *= 1e6;
|
|
for (; i < points.length && points[i] != 0xe7f7c9d; i += 4) {
|
|
let lat = points[i + 1];
|
|
let lon = points[i + 2];
|
|
let pos = [lon, lat];
|
|
if (lat >= 1073741824) {
|
|
let ac = {seen:0, seen_pos:0,};
|
|
ac.hex = (points[i] & ((1<<24) - 1)).toString(16).padStart(6, '0');
|
|
ac.hex = (points[i] & (1<<24)) ? ('~' + ac.hex) : ac.hex;
|
|
if (replay.pointsU8[4 * (i + 2)] != 0) {
|
|
ac.flight = "";
|
|
for (let j = 0; j < 8; j++) {
|
|
ac.flight += String.fromCharCode(replay.pointsU8[4 * (i + 2) + j]);
|
|
}
|
|
}
|
|
ac.squawk = (lat & 0xFFFF).toString(10).padStart(4, '0');
|
|
processAircraft(ac, false, false);
|
|
continue;
|
|
}
|
|
if (!inView(pos, ext)) {
|
|
continue;
|
|
}
|
|
|
|
lat /= 1e6;
|
|
lon /= 1e6;
|
|
pos = [lon, lat];
|
|
|
|
let type = (pointsU[i] >> 27) & 0x1F;
|
|
switch (type) {
|
|
case 0: type = 'adsb_icao'; break;
|
|
case 1: type = 'adsb_icao_nt'; break;
|
|
case 2: type = 'adsr_icao'; break;
|
|
case 3: type = 'tisb_icao'; break;
|
|
case 4: type = 'adsc'; break;
|
|
case 5: type = 'mlat'; break;
|
|
case 6: type = 'other'; break;
|
|
case 7: type = 'mode_s'; break;
|
|
case 8: type = 'adsb_other'; break;
|
|
case 9: type = 'adsr_other'; break;
|
|
case 10: type = 'tisb_trackfile'; break;
|
|
case 11: type = 'tisb_other'; break;
|
|
case 12: type = 'mode_ac'; break;
|
|
default: type = 'unknown';
|
|
}
|
|
let hex = (pointsU[i] & 0xFFFFFF).toString(16).padStart(6, '0');
|
|
hex = (pointsU[i] & 0x1000000) ? ('~' + hex) : hex;
|
|
|
|
if (icaoFilter && !icaoFilter.includes(hex))
|
|
continue;
|
|
|
|
let alt = points[i + 3] & 65535;
|
|
if (alt & 32768)
|
|
alt |= -65536;
|
|
if (alt == -123)
|
|
alt = 'ground';
|
|
else
|
|
alt *= 25;
|
|
|
|
let gs = points[i + 3] >> 16;
|
|
if (gs == -1)
|
|
gs = null;
|
|
else
|
|
gs /= 10;
|
|
|
|
let ac = {
|
|
seen: 0,
|
|
seen_pos: 0,
|
|
hex: hex,
|
|
lat: lat,
|
|
lon: lon,
|
|
alt_baro: alt,
|
|
gs: gs,
|
|
type: type,
|
|
};
|
|
processAircraft(ac, false, false);
|
|
}
|
|
|
|
if (arg != "fast") {
|
|
triggerRefresh = 1;
|
|
checkMovement();
|
|
checkRefresh();
|
|
}
|
|
replay.index = index + 1;
|
|
}
|
|
|
|
function updateIconCache() {
|
|
let item;
|
|
let tryAgain = [];
|
|
while(item = addToIconCache.pop()) {
|
|
let svgKey = item[0];
|
|
let element = item[1];
|
|
if (iconCache[svgKey] != undefined) {
|
|
continue;
|
|
}
|
|
if (!element) {
|
|
element = new Image();
|
|
element.src = item[2];
|
|
item[1] = element;
|
|
tryAgain.push(item);
|
|
continue;
|
|
}
|
|
if (!element.complete) {
|
|
console.log("moep");
|
|
tryAgain.push(item);
|
|
continue;
|
|
}
|
|
|
|
iconCache[svgKey] = element;
|
|
}
|
|
addToIconCache = tryAgain;
|
|
}
|
|
|
|
function getInactive() {
|
|
return (new Date().getTime() - lastActive) / 1000;
|
|
}
|
|
|
|
function active() {
|
|
lastActive = new Date().getTime();
|
|
}
|
|
|
|
function drawTileBorder(data) {
|
|
let southWest = ol.proj.fromLonLat([data.west, data.south]);
|
|
let south180p = ol.proj.fromLonLat([180, data.south]);
|
|
let south180m = ol.proj.fromLonLat([-180, data.south]);
|
|
let southEast = ol.proj.fromLonLat([data.east, data.south]);
|
|
let northEast = ol.proj.fromLonLat([data.east, data.north]);
|
|
let north180p = ol.proj.fromLonLat([180, data.north]);
|
|
let north180m = ol.proj.fromLonLat([-180, data.north]);
|
|
let northWest = ol.proj.fromLonLat([data.west, data.north]);
|
|
const estimateStyle = new ol.style.Style({
|
|
stroke: new ol.style.Stroke({
|
|
color: '#303030',
|
|
width: 1.5,
|
|
})
|
|
});
|
|
if (data.west < data.east) {
|
|
let tile = new ol.geom.LineString([southWest, southEast, northEast, northWest, southWest]);
|
|
let tileFeature = new ol.Feature(tile);
|
|
tileFeature.setStyle(estimateStyle);
|
|
siteCircleFeatures.addFeature(tileFeature);
|
|
} else {
|
|
let west = new ol.geom.LineString([south180p, southWest, northWest, north180p]);
|
|
let east = new ol.geom.LineString([south180m, southEast, northEast, north180m]);
|
|
let westF = new ol.Feature(west);
|
|
let eastF = new ol.Feature(east);
|
|
westF.setStyle(estimateStyle);
|
|
eastF.setStyle(estimateStyle);
|
|
siteCircleFeatures.addFeature(westF);
|
|
siteCircleFeatures.addFeature(eastF);
|
|
}
|
|
}
|
|
|
|
function updateMessageRate(data) {
|
|
if (data.messages && data.messages > 1) {
|
|
// Detect stats reset
|
|
if (MessageCountHistory.length > 0 && MessageCountHistory[MessageCountHistory.length-1].messages > data.messages) {
|
|
MessageCountHistory = [];
|
|
}
|
|
|
|
// Note the message count in the history
|
|
MessageCountHistory.push({ 'time' : data.now, 'messages' : data.messages});
|
|
|
|
if (MessageCountHistory.length > 1) {
|
|
let message_time_delta = MessageCountHistory[MessageCountHistory.length-1].time - MessageCountHistory[0].time;
|
|
let message_count_delta = MessageCountHistory[MessageCountHistory.length-1].messages - MessageCountHistory[0].messages;
|
|
if (message_time_delta > 0)
|
|
MessageRate = message_count_delta / message_time_delta;
|
|
}
|
|
|
|
// .. and clean up any old values
|
|
if ((now - MessageCountHistory[0].time) > 10)
|
|
MessageCountHistory.shift();
|
|
} else if (uuid != null && data.messages == 1) {
|
|
const cache = uuidCache[data.urlIndex] || { now: 0 };
|
|
let time_delta = now - cache.now;
|
|
if (time_delta > 0.5) {
|
|
let newCache = uuidCache[data.urlIndex] = { now: now };
|
|
let message_delta = 0;
|
|
let acs = data.aircraft;
|
|
for (let j=0; j < acs.length; j++) {
|
|
const hex = acs[j].hex;
|
|
const messages = acs[j].messages
|
|
let cachedMessages = cache[hex];
|
|
if (cachedMessages) {
|
|
message_delta += (messages - cachedMessages);
|
|
}
|
|
newCache[hex] = messages;
|
|
}
|
|
newCache.rate = message_delta / time_delta;
|
|
}
|
|
MessageRate = 0;
|
|
for (let i in uuidCache) {
|
|
const c = uuidCache[i];
|
|
MessageRate += c ? c.rate : 0;
|
|
}
|
|
} else {
|
|
MessageRate = null;
|
|
}
|
|
}
|
|
function playReplay(state){
|
|
if (!replay){
|
|
return;
|
|
}
|
|
if (state) {
|
|
replay.playing = true;
|
|
jQuery("#replayPlay").html("Pause");
|
|
replayStep();
|
|
} else {
|
|
replay.playing = false;
|
|
jQuery("#replayPlay").html("Play");
|
|
clearTimeout(refreshId);
|
|
}
|
|
};
|
|
|
|
function showReplayBar(){
|
|
console.log('showReplayBar()');
|
|
if (showingReplayBar){
|
|
// If you can see it, hide it
|
|
jQuery("#replayBar").hide();
|
|
showingReplayBar = false;
|
|
replay = null;
|
|
jQuery('#map_canvas').height('100%');
|
|
jQuery('#sidebar_canvas').height('100%');
|
|
} else {
|
|
// If it's hidden, show it and change the currently selected date to be an hour ago
|
|
jQuery("#replayBar").show();
|
|
jQuery("#replayBar").css('display', 'grid');
|
|
jQuery('#replayBar').height('100px');
|
|
jQuery('#map_canvas').height('calc(100% - 100px)');
|
|
jQuery('#sidebar_canvas').height('calc(100% - 110px)');
|
|
if (!replay) {
|
|
replay = replayDefaults(new Date());
|
|
replay.playing = false;
|
|
}
|
|
//ts.setUTCMinutes((parseInt((ts.getUTCMinutes() + 7.5)/15) * 15) % 60);
|
|
jQuery("#replayDatepicker").datepicker({
|
|
maxDate: '+1d',
|
|
dateFormat: "yy-mm-dd",
|
|
autoSize: true,
|
|
onClose: !onMobile ? null : function(dateText, inst){
|
|
jQuery("replayDatepicker").attr("disabled", false);
|
|
},
|
|
beforeShow: !onMobile ? null : function(input, inst){
|
|
jQuery("replayDatepicker").attr("disabled", true);
|
|
},
|
|
onSelect: function(dateText) {
|
|
replay.dateText = dateText;
|
|
replayJump();
|
|
}
|
|
});
|
|
|
|
jQuery('#hourSelect').slider({
|
|
step: 1,
|
|
min: 0,
|
|
max: 23,
|
|
slide: function(event, ui) {
|
|
replay.hours = ui.value;
|
|
replayOnSliderMove();
|
|
},
|
|
change: function() {
|
|
replayJump();
|
|
}
|
|
});
|
|
jQuery('#minuteSelect').slider({
|
|
step: 1,
|
|
min: 0,
|
|
max: 59,
|
|
slide: function(event, ui) {
|
|
replay.minutes = ui.value;
|
|
replayOnSliderMove();
|
|
},
|
|
change: function() {
|
|
replayJump();
|
|
}
|
|
});
|
|
const slideBase = 3.0;
|
|
jQuery('#replaySpeedSelect').slider({
|
|
value: Math.pow(replay.speed, 1 / slideBase),
|
|
step: 0.07,
|
|
min: Math.pow(1, 1 / slideBase),
|
|
max: Math.pow(250, 1 / slideBase),
|
|
slide: function(event, ui) {
|
|
replay.speed = Math.pow(ui.value, slideBase).toFixed(1);
|
|
jQuery('#replaySpeedHint').text('Speed: ' + replay.speed + 'x');
|
|
},
|
|
change: function(event, ui) {
|
|
replayStep();
|
|
},
|
|
});
|
|
jQuery('#replaySpeedHint').text('Speed: ' + replay.speed + 'x');
|
|
showingReplayBar = true;
|
|
}
|
|
};
|
|
|
|
function timeoutFetch() {
|
|
fetchData();
|
|
timers.checkMove = setTimeout(timeoutFetch, Math.max(RefreshInterval, 10000));
|
|
if (lastReap - now > 90000)
|
|
reaper();
|
|
}
|
|
|
|
function handleVisibilityChange() {
|
|
const prevHidden = tabHidden;
|
|
if (document[hideName])
|
|
tabHidden = true;
|
|
else
|
|
tabHidden = false;
|
|
|
|
if (tabHidden) {
|
|
clearIntervalTimers();
|
|
if (!globeIndex) {
|
|
timeoutFetch();
|
|
}
|
|
}
|
|
|
|
// tab is no longer hidden
|
|
if (!tabHidden && prevHidden) {
|
|
|
|
globeRateUpdate();
|
|
clearIntervalTimers();
|
|
setIntervalTimers();
|
|
|
|
active();
|
|
|
|
refresh();
|
|
fetchData();
|
|
|
|
if (showTrace)
|
|
return;
|
|
if (heatmap)
|
|
return;
|
|
|
|
if (!globeIndex)
|
|
return;
|
|
|
|
let count = 0;
|
|
if (multiSelect && !SelectedAllPlanes) {
|
|
for (let i = 0; i < PlanesOrdered.length; ++i) {
|
|
let plane = PlanesOrdered[i];
|
|
if (plane.selected) {
|
|
getTrace(plane, plane.icao, {});
|
|
if (count++ > 20)
|
|
break;
|
|
}
|
|
}
|
|
} else if (SelectedPlane) {
|
|
getTrace(SelectedPlane, SelectedPlane.icao, {});
|
|
}
|
|
}
|
|
}
|
|
|
|
let hideName;
|
|
function initVisibilityChange() {
|
|
// Set the name of the hidden property and the change event for visibility
|
|
let visibilityChange;
|
|
if (typeof document.hidden !== "undefined") { // Opera 12.10 and Firefox 18 and later support
|
|
hideName = "hidden";
|
|
visibilityChange = "visibilitychange";
|
|
} else if (typeof document.msHidden !== "undefined") {
|
|
hideName = "msHidden";
|
|
visibilityChange = "msvisibilitychange";
|
|
} else if (typeof document.webkitHidden !== "undefined") {
|
|
hideName = "webkitHidden";
|
|
visibilityChange = "webkitvisibilitychange";
|
|
}
|
|
// Warn if the browser doesn't support addEventListener or the Page Visibility API
|
|
if (typeof document.addEventListener === "undefined" || hideName === undefined) {
|
|
console.log("hidden tab handler requires a browser that supports the Page Visibility API.");
|
|
} else {
|
|
// Handle page visibility change
|
|
document.addEventListener(visibilityChange, handleVisibilityChange, false);
|
|
}
|
|
handleVisibilityChange();
|
|
}
|
|
// for debugging visibilitychange:
|
|
function testHide() {
|
|
Object.defineProperty(window.document,'hidden',{get:function(){return true;},configurable:true});
|
|
Object.defineProperty(window.document,'visibilityState',{get:function(){return 'hidden';},configurable:true});
|
|
window.document.dispatchEvent(new Event('visibilitychange'));
|
|
}
|
|
function testUnhide() {
|
|
Object.defineProperty(window.document,'hidden',{get:function(){return false;},configurable:true});
|
|
Object.defineProperty(window.document,'visibilityState',{get:function(){return 'visible';},configurable:true});
|
|
window.document.dispatchEvent(new Event('visibilitychange'));
|
|
}
|
|
|
|
function selectClosest() {
|
|
if (!loadFinished)
|
|
return;
|
|
let closest = null;
|
|
let closestDistance = null;
|
|
checkMovement();
|
|
for (let key in PlanesOrdered) {
|
|
const plane = PlanesOrdered[key];
|
|
if (!closest)
|
|
closest = plane;
|
|
if (plane.position == null || !plane.visible)
|
|
continue;
|
|
const dist = ol.sphere.getDistance([CenterLon, CenterLat], plane.position);
|
|
if (dist == null || isNaN(dist))
|
|
continue;
|
|
if (closestDistance == null || dist < closestDistance) {
|
|
closestDistance = dist;
|
|
closest = plane;
|
|
}
|
|
}
|
|
if (!closest)
|
|
return;
|
|
selectPlaneByHex(closest.icao, {noDeselect: true, follow: FollowSelected,});
|
|
}
|
|
function setAutoselect() {
|
|
clearInterval(timers.autoselect);
|
|
if (!autoselect)
|
|
return;
|
|
timers.autoselect = window.setInterval(selectClosest, 5000);
|
|
selectClosest();
|
|
}
|
|
function registrationLink(plane) {
|
|
if (plane.icaorange.country === 'Brazil') {
|
|
return `https://sistemas.anac.gov.br/aeronaves/cons_rab_resposta_en.asp?textMarca=${plane.registration}`;
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
|
|
//simple jquery plugin to only update the text when it changes
|
|
jQuery.fn.updateText = function (text) {
|
|
this.text() !== String(text) && this.text(text);
|
|
}
|
|
|
|
function zeroPad(num, size) {
|
|
var s = num + "";
|
|
while (s.length < size) s = "0" + s;
|
|
return s;
|
|
}
|
|
|
|
// Converts "hiccup"-style structures (https://github.com/weavejester/hiccup)
|
|
// to XML.
|
|
function hiccup(node) {
|
|
if (Array.isArray(node)) {
|
|
const [tag, attribs, ...children] = node;
|
|
let attribStrings = [];
|
|
for (const prop in attribs) {
|
|
if (!attribs.hasOwnProperty(prop) || attribs[prop] === undefined) {
|
|
continue;
|
|
}
|
|
attribStrings.push(`${prop}="${attribs[prop]}"`);
|
|
}
|
|
let xml = `<${tag} ${attribStrings.join(' ')}>`;
|
|
for (const child of children) {
|
|
xml += hiccup(child);
|
|
}
|
|
xml += `</${tag}>\n`;
|
|
return xml;
|
|
} else {
|
|
return '' + node;
|
|
}
|
|
}
|
|
|
|
// Prompts a browser to download a data: URL.
|
|
function download(name, contentType, data) {
|
|
var link = document.createElement("a");
|
|
link.download = name;
|
|
link.href = 'data:' + contentType + ',' + encodeURIComponent(data);
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
}
|
|
|
|
function baseExportFilenameForAircrafts(aircrafts) {
|
|
return aircrafts.map((a) => (a.registration || a.icao).toUpperCase()).join('-');
|
|
}
|
|
|
|
// Returns an array of {pos, alt, ts} for an aircraft.
|
|
function coordsForExport(plane) {
|
|
let coords = [];
|
|
let numSegs = plane.track_linesegs.length;
|
|
let segs = plane.track_linesegs;
|
|
let runningAverage = 0;
|
|
let lastTimestamp = 0;
|
|
let delta;
|
|
//console.log(kmlStyle);
|
|
if (kmlStyle == 'geom_avg') {
|
|
//console.log('averaging');
|
|
const avgWindow = 8;
|
|
for (let i = 0; i < numSegs; i++) {
|
|
const seg = segs[i];
|
|
const geom = seg.alt_geom;
|
|
const baro = seg.alt_real;
|
|
let geomOffset = null;
|
|
if (!seg.ground && baro != null && geom != null) {
|
|
seg.geomOffset = geom - baro;
|
|
delta = (seg.geomOffset - runningAverage);
|
|
if (delta <= 50) {
|
|
runningAverage += delta * Math.min(1, (seg.ts - lastTimestamp) / avgWindow);
|
|
} else {
|
|
runningAverage = seg.geomOffset;
|
|
}
|
|
seg.geomOffAverage = runningAverage;
|
|
lastTimestamp = seg.ts;
|
|
}
|
|
}
|
|
lastTimestamp = 1e12;
|
|
for (let i = numSegs - 1; i >= 0; i--) {
|
|
const seg = segs[i];
|
|
const geom = seg.alt_geom;
|
|
const baro = seg.alt_real;
|
|
let geomOffset = null;
|
|
if (seg.geomOffset != null) {
|
|
delta = (seg.geomOffset - runningAverage);
|
|
if (delta <= 50) {
|
|
runningAverage += delta * Math.min(1, (lastTimestamp - seg.ts) / avgWindow);
|
|
} else {
|
|
runningAverage = seg.geomOffset;
|
|
}
|
|
//console.log(seg.geomOffAverage + ' ' + runningAverage);
|
|
seg.geomOffAverage = (seg.geomOffAverage + runningAverage) / 2;
|
|
lastTimestamp = seg.ts;
|
|
}
|
|
}
|
|
}
|
|
for (let i = 0; i < numSegs; i++) {
|
|
const seg = segs[i];
|
|
const pos = seg.position;
|
|
if (pos) {
|
|
let alt = null;
|
|
const baro = seg.alt_real;
|
|
const geom = seg.alt_geom;
|
|
const geomOffAverage = seg.geomOffAverage;
|
|
let using_baro = false;
|
|
if (kmlStyle == 'geom_avg' && geomOffAverage != null) {
|
|
const betterGeom = baro + geomOffAverage;
|
|
alt = Math.round(betterGeom * 0.3048); // convert ft to m
|
|
} else if (kmlStyle != 'baro' && geom != null) {
|
|
alt = Math.round(geom * 0.3048); // convert ft to m
|
|
} else if (kmlStyle == 'baro' && baro != null && baro != 'ground') {
|
|
alt = Math.round(baro * 0.3048);
|
|
using_baro = true;
|
|
}
|
|
if (seg.ground) {
|
|
alt = "ground";
|
|
} else if (alt != null && egmLoaded && !using_baro) {
|
|
// alt is in meters at this point
|
|
alt = Math.round(egm96.ellipsoidToEgm96(pos[1], pos[0], alt));
|
|
}
|
|
|
|
const ts = new Date(seg.ts * 1000.0);
|
|
if (alt == null) {
|
|
console.log(`Skipping, no altitude: ${i} ${pos} ${ts}`);
|
|
continue;
|
|
}
|
|
//console.log(`exporting coord: ${i} ${pos} ${alt} ${ts}`);
|
|
coords.push({ pos: pos, alt: alt, ts: ts});
|
|
} else {
|
|
console.log(`Skipping ${i}`);
|
|
}
|
|
}
|
|
return coords;
|
|
}
|
|
|
|
// We use this to give each aircraft a different color track in a
|
|
// multi-select export scenario. From colorbrewer, but I moved the red
|
|
// to be first.
|
|
const EXPORT_RGB_COLORS = [
|
|
'e31a1c',
|
|
'a6cee3',
|
|
'1f78b4',
|
|
'b2df8a',
|
|
'33a02c',
|
|
'fb9a99',
|
|
'fdbf6f',
|
|
'ff7f00',
|
|
'cab2d6',
|
|
'6a3d9a',
|
|
'ffff99',
|
|
'b15928'
|
|
];
|
|
|
|
// Converts "rrggbb" colors to KML format, "aabbggrr".
|
|
function RGBColorToKMLColor(c) {
|
|
return 'ff' + c.substring(4, 6) + c.substring(2, 4) + c.substring(0, 2);
|
|
}
|
|
|
|
// Returns an array of selected planes, ordered by registration-or-ICAO.
|
|
function selectedPlanes() {
|
|
const planes = [];
|
|
for (let key in SelPlanes) {
|
|
let plane = SelPlanes[key];
|
|
if (plane.selected) {
|
|
planes.push(plane);
|
|
}
|
|
}
|
|
planes.sort((a, b) => {
|
|
const keyA = (a.registration || a.icao).toUpperCase();
|
|
const keyB = (b.registration || b.icao).toUpperCase();
|
|
if (keyA < keyB) return -1;
|
|
if (keyA > keyB) return 1;
|
|
return 0;
|
|
});
|
|
return planes;
|
|
}
|
|
|
|
// Exports currently selected aircraft as KML.
|
|
|
|
let egmScript = null;
|
|
let egmLoaded = false;
|
|
function loadEGM() {
|
|
if (egmScript) {
|
|
return null;
|
|
}
|
|
egmScript = document.createElement('script');
|
|
egmScript.src = "libs/egm96-universal-1.1.0.min.js";
|
|
egmScript.addEventListener('load', function() {
|
|
egmLoaded = true;
|
|
});
|
|
document.body.appendChild(egmScript);
|
|
return egmScript;
|
|
}
|
|
function adjust_geom_alt(alt, pos) {
|
|
if (geomUseEGM && egmLoaded) {
|
|
if (alt == null) {
|
|
return alt;
|
|
}
|
|
return egm96.ellipsoidToEgm96(pos[1], pos[0], alt * 0.3048) / 0.3048;
|
|
} else {
|
|
return alt;
|
|
}
|
|
}
|
|
let kmlStyle = '';
|
|
function exportKML(altStyle) {
|
|
if (altStyle) {
|
|
kmlStyle = altStyle;
|
|
}
|
|
if (!egmLoaded) {
|
|
let egm = loadEGM();
|
|
if (egm) {
|
|
egm.addEventListener('load', function() {
|
|
exportKML();
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
|
|
const planes = selectedPlanes();
|
|
const folders = [];
|
|
for (let planeIndex = 0; planeIndex < planes.length; planeIndex++) {
|
|
const plane = planes[planeIndex];
|
|
let folder = ["Folder", {},
|
|
["name", {}, `${(plane.registration || plane.icao).toUpperCase()} track`]
|
|
];
|
|
const coords = coordsForExport(plane);
|
|
let sections = [];
|
|
let currentSection = null;
|
|
let lastGround = null;
|
|
let lastC = null;
|
|
for (let i in coords) {
|
|
const c = coords[i];
|
|
const ground = (c.alt == "ground");
|
|
if (ground !== lastGround) {
|
|
// when changing between airborne and ground, create new section
|
|
if (lastC && currentSection) {
|
|
// double up last coordinate to work around strange google earth transparency
|
|
currentSection.coords.push(lastC);
|
|
}
|
|
currentSection = { ground: ground, coords: [] };
|
|
sections.push(currentSection);
|
|
}
|
|
lastGround = ground;
|
|
if (ground) {
|
|
c.alt = 0; // set KML altitude to zero
|
|
}
|
|
currentSection.coords.push(c);
|
|
lastC = c;
|
|
}
|
|
if (lastC && currentSection) {
|
|
// double up last coordinate to work around strange google earth transparency
|
|
currentSection.coords.push(lastC);
|
|
}
|
|
for (let i in sections) {
|
|
console.log("section " + i);
|
|
const s = sections[i];
|
|
const coords = s.coords;
|
|
const ground = s.ground;
|
|
const whenObjs = coords.map((c) => {
|
|
const date = `${c.ts.getUTCFullYear()}-${zeroPad(c.ts.getUTCMonth() + 1, 2)}-${zeroPad(c.ts.getUTCDate(), 2)}`;
|
|
const time = `T${zeroPad(c.ts.getUTCHours(), 2)}:${zeroPad(c.ts.getUTCMinutes(), 2)}:${zeroPad(c.ts.getUTCSeconds(), 2)}.${zeroPad(c.ts.getUTCMilliseconds(), 3)}Z`;
|
|
return ["when", {}, date + time];
|
|
});
|
|
const coordObjs = coords.map((c) => {
|
|
return ["gx:coord", {}, `${c.pos[0]} ${c.pos[1]} ${c.alt}`];
|
|
});
|
|
// splice together the xml track with / without altitude mode
|
|
// clamptoground is google earth default while other programs error on having that option set specifically
|
|
// so let google earth default to clamp to ground for ground track
|
|
let xmlTrack = ["gx:Track", {}];
|
|
if (!ground) {
|
|
xmlTrack.push(["altitudeMode", {}, "absolute"]);
|
|
}
|
|
xmlTrack = xmlTrack.concat([
|
|
["extrude", {}, ground ? "0" : "1"],
|
|
...whenObjs,
|
|
...coordObjs
|
|
]);
|
|
folder.push(
|
|
["Placemark", {},
|
|
["name", {}, (plane.registration || plane.icao).toUpperCase()],
|
|
["Style", {},
|
|
["LineStyle", {},
|
|
["color", {}, RGBColorToKMLColor(EXPORT_RGB_COLORS[planeIndex % EXPORT_RGB_COLORS.length])],
|
|
["width", {}, 4]
|
|
],
|
|
["IconStyle", {},
|
|
["Icon", {},
|
|
["href", {}, "http://maps.google.com/mapfiles/kml/shapes/airports.png"]
|
|
]
|
|
]
|
|
],
|
|
xmlTrack
|
|
]
|
|
);
|
|
}
|
|
folders.push(folder);
|
|
}
|
|
const filename = baseExportFilenameForAircrafts(planes);
|
|
const prologue = '<?xml version="1.0" encoding="UTF-8"?>\n';
|
|
const xmlObj = ["kml", {
|
|
"xmlns": "http://www.opengis.net/kml/2.2",
|
|
"xmlns:gx": "http://www.google.com/kml/ext/2.2"
|
|
},
|
|
["Folder", {},
|
|
...folders
|
|
]
|
|
];
|
|
const xml = prologue + hiccup(xmlObj);
|
|
let styleName = '';
|
|
if (kmlStyle == 'geom') { styleName = 'EGM96'; };
|
|
if (kmlStyle == 'geom_avg') { styleName = 'EGM96_avg'; };
|
|
if (kmlStyle == 'baro') { styleName = 'press_alt_uncorrected'; };
|
|
//console.log(kmlStyle + ' ' + styleName);
|
|
download(
|
|
filename + '-track-' + styleName + '.kml',
|
|
'application/vnd.google-earth.kml+xml',
|
|
xml);
|
|
}
|
|
|
|
function deleteTraces() {
|
|
for (let i in PlanesOrdered) {
|
|
let plane = PlanesOrdered[i];
|
|
delete plane.recentTrace;
|
|
delete plane.fullTrace;
|
|
}
|
|
}
|
|
|
|
function setPictureVisibility() {
|
|
showPictures = planespottersAPI || planespottingAPI;
|
|
if (showPictures) {
|
|
jQuery('#photo_container').removeClass('hidden');
|
|
} else {
|
|
jQuery('#photo_container').addClass('hidden');
|
|
}
|
|
if (planespottersLinks && !showPictures) {
|
|
jQuery('#photoLinkRow').removeClass('hidden');
|
|
} else {
|
|
jQuery('#photoLinkRow').addClass('hidden');
|
|
}
|
|
}
|
|
|
|
// just an idea, unused
|
|
let infoBits = {
|
|
type: {
|
|
head: 'Type:',
|
|
title: '4 character ICAO type code (i.e.: A320,B738,G550)',
|
|
value: function(plane) { return plane.icaoType || 'n/a'; },
|
|
},
|
|
};
|
|
|
|
function geoFindEnabled() {
|
|
return (!SiteOverride && (globeIndex || uuid || askLocation) && (window && window.location && window.location.protocol == 'https:'));
|
|
}
|
|
|
|
function _printTrace(trace) {
|
|
for (let i = 0; i < trace.length; i++) {
|
|
const state = trace[i];
|
|
const timestamp = state[0];
|
|
let stale = state[6] & 1;
|
|
const leg_marker = state[6] & 2;
|
|
console.log(zuluTime(new Date(timestamp * 1000)) + ' ' + (state[1] + ',' + state[2]).padStart(26, ' ') + ' ' + String(state[3]).padStart(6, ' ') + ' ' + state[6]);
|
|
}
|
|
}
|
|
|
|
function printTrace() {
|
|
console.log('full trace');
|
|
_printTrace(SelectedPlane.fullTrace.trace);
|
|
console.log('recent trace');
|
|
_printTrace(SelectedPlane.recentTrace.trace);
|
|
}
|
|
|
|
|
|
// Create a "hidden" input
|
|
let shareLinkInput = document.createElement("input");
|
|
// Append it to the body
|
|
document.body.appendChild(shareLinkInput);
|
|
|
|
function copyShareLink() {
|
|
// Assign shareLinkInput the value we want to copy
|
|
shareLinkInput.setAttribute("value", shareLink);
|
|
|
|
// Highlight its content
|
|
shareLinkInput.select();
|
|
// Copy the highlighted text
|
|
document.execCommand("copy");
|
|
// deselect input field
|
|
shareLinkInput.blur();
|
|
|
|
copyLinkTime = new Date().getTime();
|
|
copiedIcao = SelectedPlane.icao;
|
|
setSelectedIcao();
|
|
}
|
|
|
|
let copyLinkTime = 0;
|
|
let copiedIcao = null;
|
|
|
|
function setSelectedIcao() {
|
|
const selected = SelectedPlane;
|
|
if (selected.icao == selIcao && copiedIcao == null) {
|
|
return;
|
|
}
|
|
selIcao = selected.icao;
|
|
let hex_html = "<span style='font-family: monospace;' class=identSmall>Hex:" + NBSP + selected.icao.toUpperCase() + "</span>";
|
|
if (globeIndex || shareBaseUrl) {
|
|
if (copiedIcao && (copiedIcao != selected.icao || new Date().getTime() - copyLinkTime > 2000)) {
|
|
copiedIcao = null;
|
|
}
|
|
let copy_link_text = (copiedIcao != null) ? "Copied" : ("Copy" + NBSP + "Link");
|
|
let icao_link = "<span class=identSmall><a class='link identSmall' target=\"_blank\" href=\"" + shareLink +
|
|
"\" onclick=\"copyShareLink(); return false;\">" + copy_link_text + "</a></span>";
|
|
hex_html = hex_html + NBSP + NBSP + NBSP + icao_link;
|
|
}
|
|
jQuery('#selected_icao').html(hex_html);
|
|
|
|
jQuery('a.identSmall').prop('href',shareLink);
|
|
}
|
|
|
|
function mapTypeSettings() {
|
|
if (MapType_tar1090.startsWith('maptiler_sat') || MapType_tar1090.startsWith('maptiler_hybrid')) {
|
|
layerExtraDim = -0.25;
|
|
} else if (MapType_tar1090.startsWith('carto_raster')) {
|
|
layerExtraDim = -0.15;
|
|
layerExtraContrast = 0.6;
|
|
} else if (MapType_tar1090.startsWith('carto_light')) {
|
|
layerExtraDim = -0.05;
|
|
layerExtraContrast = 0.2;
|
|
} else {
|
|
layerExtraDim = 0;
|
|
layerExtraContrast = 0;
|
|
}
|
|
}
|
|
|
|
function requestBoxString() {
|
|
if (!mapIsVisible && lastRequestBox) {
|
|
return lastRequestBox;
|
|
}
|
|
let extent = getViewOversize(1.03);
|
|
let minLon = extent.minLon.toFixed(6);
|
|
let maxLon = extent.maxLon.toFixed(6);
|
|
if (Math.abs(extent.extent[2] - extent.extent[0]) > 40075016) { // all longtitudes in view
|
|
minLon = -180, maxLon = 180;
|
|
}
|
|
return `${extent.minLat.toFixed(6)},${extent.maxLat.toFixed(6)},${minLon},${maxLon}`;
|
|
}
|
|
|
|
if (adsbexchange && window.location.hostname.startsWith('inaccurate')) {
|
|
jQuery('#inaccurate_warning').removeClass('hidden');
|
|
}
|
|
|
|
parseURLIcaos();
|
|
initialize();
|