';
}
let columns = createOrderedColumns();
let activeCols = null;
let initializing = true;
let planeRowTemplate = 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 = function () {
activeCols = [];
for (let i in columns) {
let col = columns[i];
if (col.visible || !mapIsVisible) {
activeCols.push(col);
}
}
for (let i = 0; i < g.planesOrdered.length; ++i) {
g.planesOrdered[i].destroyTR();
}
let table = '';
table += '';
table += '
';
for (let i in activeCols) {
let col = activeCols[i];
table += '
'+ col.header() +'
';
}
table += '
';
table += '';
table += '
';
table += '';
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 += '';
}
planeRowTemplate.innerHTML = template;
if (!initializing) {
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 (!loadFinished) {
return;
}
//console.trace();
if (initializing) {
planeMan.redraw();
initializing = false;
}
const atime = false;
atime && console.time("planeMan.refresh()");
const ctime = false; // gets enabled for debugging table refresh speed
// globeTableLimit = 1000; for testing performance
ctime && console.time("planeMan.refresh()");
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 < g.planesOrdered.length; ++i) {
const plane = g.planesOrdered[i];
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.remove();
tbody = newBody;
ctime && console.timeEnd("DOM2");
ctime && console.timeEnd("planeMan.refresh()");
atime && console.timeEnd("planeMan.refresh()");
}
//
// ---- table sorting begin ----
//
planeMan.sortId = '';
planeMan.sortCompare = null;
planeMan.sortExtract = null;
planeMan.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 = planeMan.sortAscending ? planeMan.sortCompare(xv,yv) : planeMan.sortCompare(yv,xv);
if (c !== 0) return c;
return x._sort_pos - y._sort_pos;
}
function resortTable(pList) {
if (!planeMan.sortExtract)
return;
if (globeIndex) {
// don't presort for globeIndex
}
// presort by dataSource
else if (planeMan.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 (planeMan.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 = planeMan.sortExtract(pList[i]);
}
} else {
for (let i = 0; i < pList.length; ++i) {
pList[i]._sort_pos = i;
pList[i]._sort_value = planeMan.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) {
loStore['sortCol'] = id;
if (id === planeMan.sortId) {
planeMan.sortAscending = !planeMan.sortAscending;
g.planesOrdered.reverse(); // this correctly flips the order of rows that compare equal
}
loStore['sortAscending'] = planeMan.sortAscending ? 'true' : '';
planeMan.sortId = id;
planeMan.sortCompare = sc;
planeMan.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(``);
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 || {}));
//
// g.planes table end
//
let lastSelected = null;
function deselect(plane) {
if (!plane || !plane.selected)
return;
plane.selected = false;
const index = SelPlanes.indexOf(plane);
if (index > -1) {
SelPlanes.splice(index, 1);
}
lastSelected = plane;
if (plane == SelectedPlane) {
if (SelPlanes.length > 0) {
sp = SelectedPlane = SelPlanes[0];
} else {
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();
options = options || {};
console.log(`SELECTING ${hex} follow: ${options.follow}`);
//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 = g.planes[hex];
const multiDeselect = multiSelect && newPlane && newPlane.selected && !onlySelected;
if (!options.noFetch && (globeIndex || showTrace || haveTraces) && 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 (multiDeselect) {
deselect(newPlane);
newPlane = null;
hex = null;
}
} else if (oldPlane) {
// normal deselect
if (oldPlane != newPlane) {
deselect(oldPlane);
oldPlane = null;
}
if (oldPlane == newPlane) {
console.log('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;
// disable this for the moment
if (0 && globeIndex) {
for (let i in g.planesOrdered) {
let plane = g.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() {
geoFindMe().always(function() {
if (SitePosition) {
CenterLon = SiteLon;
CenterLat = SiteLat;
} else {
CenterLon = DefaultCenterLon;
CenterLat = DefaultCenterLat;
}
// Reset loStore values and map settings
lopaStore['CenterLat'] = CenterLat
lopaStore['CenterLon'] = CenterLon
//lopaStore['zoomLvl'] = g.zoomLvl = DefaultZoomLvl;
// Set and refresh
//OLMap.getView().setZoom(g.zoomLvl);
//console.log('resetMap setting center ' + [CenterLat, CenterLon]);
OLMap.getView().setCenter(ol.proj.fromLonLat([CenterLon, CenterLat]));
OLMap.getView().setRotation(g.mapOrientation);
//selectPlaneByHex(null,false);
jQuery("#update_error").css('display','none');
});
}
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 otherwise use value previously set defaults.js or config.js
if (loStore.getItem('displayUnits')) {
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
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 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, noRefresh) {
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 && noRefresh != "noRefresh")
refreshFilter();
fetchData({force: true});
}
function toggleMilitary() {
onlyMilitary = !onlyMilitary;
buttonActive('#U', onlyMilitary);
refreshFilter();
active();
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 {
let currentDimPercentage = mapDimPercentage * layerDimFactor;
let currentContrastPercentage = mapContrastPercentage + layerExtraContrast;
if (!toggles['MapDim'].state) {
// slight dim even if disabled
currentDimPercentage /= 4;
currentContrastPercentage /= 4;
}
const dim = currentDimPercentage * (1 + 0.25 * toggles['darkerColors'].state);
const contrast = currentContrastPercentage * (1 + 0.1 * toggles['darkerColors'].state);
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 += '';
}
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));
jQuery('#altitude_chart').show();
});
}
altitudeChart.render = function () {
if (toggles['altitudeChart'].state) {
runAfterLoad(loadLegend);
} else {
jQuery('#altitude_chart').hide();
}
}
altitudeChart.init = function () {
let chartOn = (onMobile ? false : altitudeChartDefaultState);
if (usp.has('altitudeChart')) {
chartOn = Boolean(parseInt(usp.get('altitudeChart')));
}
new Toggle({
key: "altitudeChart",
display: "Altitude Chart",
container: "#settingsRight",
init: chartOn,
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 = g.planesOrdered[Math.floor(Math.random()*g.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(arg) {
if (arg == 'enable') {
tableInView = true;
} else if (arg == 'disable') {
tableInView = false;
} else if (!globeIndex) {
tableInView = !tableInView;
}
TAR.planeMan.refresh();
if (!globeIndex) {
loStore['tableInView'] = tableInView;
}
jQuery('#with_positions').text(tableInView ? "On Screen:" : "With Position:");
buttonActive('#V', tableInView);
}
function toggleLabels() {
g.enableLabels = !g.enableLabels;
loStore['enableLabels'] = g.enableLabels;
for (let key in g.planesOrdered) {
g.planesOrdered[key].updateMarker();
}
refreshFeatures();
buttonActive('#L', g.enableLabels);
if (showTrace)
remakeTrails();
}
function toggleExtendedLabels(options) {
if (isNaN(g.extendedLabels))
g.extendedLabels = 0;
options = options || {};
if (!options.noIncrement) {
g.extendedLabels++;
}
g.extendedLabels %= 4;
//console.log(extendedLabels);
loStore['extendedLabels'] = g.extendedLabels;
for (let key in g.planesOrdered) {
g.planesOrdered[key].updateMarker();
}
buttonActive('#O', g.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 (!g.airport_cache) {
jQuery.getJSON(databaseFolder + "/airport-coords.js")
.done(function(data) {
g.airport_cache = data;
onJump();
});
return;
}
coords = g.airport_cache[airport];
}
if (coords) {
console.log("jumping to: " + coords[0] + " " + coords[1]);
OLMap.getView().setCenter(ol.proj.fromLonLat([coords[1], coords[0]]));
if (g.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 && haveTraces) {
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 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 '' + linkText + '';
}
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();
}
const filters = {};
const filter_list = [];
const filters_active = [];
function Filter(arg) {
this.key = arg.key;
this.field = arg.field;
this.name = arg.name;
this.tbody = document.getElementById(arg.table).getElementsByTagName('tbody')[0];
this.id = 'filters_' + this.key;
this.sid = '#' + this.id;
filters[this.key] = this;
filter_list.push(this);
this.init();
}
Filter.prototype.update = function(e) {
if (e) {
e.preventDefault();
}
this.input.blur();
const val = this.input.val().trim();
this.set(val);
return false;
}
Filter.prototype.set = function(val) {
this.input.val(val);
this.pattern = val;
this.PATTERN = this.pattern.toUpperCase();
const list_index = filters_active.indexOf(this);
if (val && list_index < 0) {
filters_active.push(this);
}
if (!val && list_index >= 0) {
filters_active.splice(list_index);
}
refreshFilter();
}
Filter.prototype.reset = function(e) {
if (e) {
e.preventDefault();
}
this.set("");
return false;
}
Filter.prototype.init = function() {
// don't F directly with the innerhtml of the body because it will drop event listeners / recreate dom elements
const row = this.tbody.insertRow();
row.innerHTML =
`
'
;
this.input = jQuery(this.sid + '_input');
this.form = document.getElementById(this.id)
this.form.onsubmit = (e) => { return this.update(e); };
jQuery(this.sid + '_reset').click((e) => { return this.reset(e); });
}
function initFilters() {
initSourceFilter(tableColors.unselected);
initFlagFilter(tableColors.unselected);
new Filter({
key: 'callsign',
field: 'name',
name: 'callsign',
table: "filterTable",
});
new Filter({
key: 'squawk',
field: 'squawk',
name: 'squawk',
table: "filterTable",
});
new Filter({
key: 'type',
field: 'icaoType',
name: 'type code',
table: "filterTable",
});
new Filter({
key: 'description',
field: 'typeDescription',
name: 'type description',
table: "filterTable",
});
new Filter({
key: 'icao',
field: 'icao',
name: 'ICAO hex id',
table: "filterTable",
});
new Filter({
key: 'registration',
field: 'registration',
name: 'registration',
table: 'filterTable3'
});
if (routeApiUrl) {
new Filter({
key: 'route',
field: 'routeString',
name: 'route',
table: 'filterTable3'
});
}
new Filter({
key: 'country',
field: 'country',
name: 'country of registration',
table: 'filterTable3'
});
new Filter({
key: 'category',
field: 'category',
name: 'category (A3,B0,..)',
table: 'filterTable3'
});
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);
}
for (const filter of filter_list) {
if (usp.has(`filter${filter.key}`)) {
filter.set(usp.get(`filter${filter.key}`));
}
}
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 getFlightAwareModeSLink(code, ident, linkText) {
if (code !== null && code.length > 0 && code[0] !== '~' && code !== "000000") {
if (!linkText) {
linkText = "FlightAware: " + code.toUpperCase();
}
let linkHtml = "" + linkText + "";
return linkHtml;
}
return "";
}
function getPhotoLink(ac) {
if (jetphotoLinks) {
if (ac.registration == null || ac.registration == "")
return "";
return "Jetphotos";
} else if (flightawareLinks) {
if (ac.registration == null || ac.registration == "")
return "";
return "FA Photos";
} else if (showPictures) {
return "View on g.planespotters";
}
}
// 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 g.planesOrdered) {
const plane = g.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 < g.planesOrdered.length; i++) {
let plane = g.planesOrdered[i];
//console.log(plane);
if (plane.visible) {
list[Math.floor(4*i/g.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;
g.zoomLvl = OLMap.getView().getZoom();
checkScale();
// small zoomstep, no need to change aircraft scaling
if (!init && Math.abs(g.zoomLvl-g.zoomLvlCache) < 0.4)
return;
lopaStore['zoomLvl'] = g.zoomLvl;
g.zoomLvlCache = g.zoomLvl;
if (!init && showTrace)
updateAddressBar();
checkPointermove();
}
function checkScale() {
if (g.zoomLvl > markerZoomDivide) {
iconSize = markerBig;
} else if (g.zoomLvl > markerZoomDivide - 1) {
iconSize = markerSmall;
} else {
iconSize = markerSmall;
if (aircraftShown > 700) {
iconSize *= 0.9;
}
}
// 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 = `${labelStyle} ${(12 * globalScale * labelScale)}px/${(14 * globalScale * labelScale)}px ${labelFamily}`;
checkScale();
setLineWidth();
if (!init) {
refreshFeatures();
refreshSelected();
refreshHighlighted();
remakeTrails();
}
}
function checkPointermove() {
if ((webgl || g.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;
}
lopaStore['CenterLon'] = CenterLon = center[0];
lopaStore['CenterLat'] = CenterLat = center[1];
if (!init) {
updateAddressBar();
}
if (rawCenter[0] < OLProjExtent[0] || rawCenter[0] > OLProjExtent[2]) {
OLMap.getView().setCenter(ol.proj.fromLonLat(center));
refresh();
}
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;
if (!g.firstFetchDone) {
return;
}
let currentTime = Date.now()/1000;
if (currentTime > g.route_next_lookup && !g.route_check_in_flight) {
// check if it's time to send a batch of request to the API server
g.route_next_lookup = currentTime + 1;
routeDoLookup();
}
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 (!g.firstFetchDone) {
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(redraw) {
lastRefresh = new Date().getTime();
refreshZoom = getZoom();
refreshCenter = getCenter();
if (replay) {
for (let i in SelPlanes) {
const plane = SelPlanes[i];
plane.processTrace();
}
}
// before planeman refresh / mapRefresh
updateVisible();
mapRefresh(redraw);
//console.time("refreshTable");
TAR.planeMan.refresh();
//console.timeEnd("refreshTable");
refreshSelected();
refreshHighlighted();
triggerRefresh = 0;
}
function refreshFilter() {
if (filterTracks)
remakeTrails();
refresh(true);
drawHeatmap();
if (toggles.shareFilters && toggles.shareFilters.state) {
updateAddressBar();
}
}
function updateVisible() {
if (mapIsVisible || !lastRenderExtent) {
lastRenderExtent = getRenderExtent();
}
aircraftShown = 0;
for (let i in g.planesOrdered) {
const plane = g.planesOrdered[i];
plane.updateVisible();
aircraftShown += (plane.visible && plane.inView);
}
checkScale();
}
function mapRefresh(redraw) {
if (!mapIsVisible || heatmap)
return;
let addToMap = [];
let nMapPlanes = 0;
let count = 0;
if (globeIndex && !icaoFilter) {
for (let i in g.planesOrdered) {
count++;
const plane = g.planesOrdered[i];
delete plane.glMarker;
// disable mobile limitations when using webGL
if (plane.selected || (plane.inView && plane.visible && (!onMobile || webgl || (nMapPlanes < 150 && (!plane.onGround || g.zoomLvl > 10))))) {
addToMap.push(plane);
nMapPlanes++;
} else {
plane.markerDrawn && plane.clearMarker();
!SelectedAllPlanes && plane.linesDrawn && plane.clearLines();
}
}
} else {
for (let i in g.planesOrdered) {
const plane = g.planesOrdered[i];
delete plane.glMarker;
addToMap.push(plane);
}
}
//console.log('planes on map: ' + nMapPlanes + ' / ' + count);
// 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 order
addToMap.sort(function(x, y) { return x.zIndex - y.zIndex; });
//console.log('maprefresh(): ' + addToMap.length);
if (webgl) {
webglFeatures.clear();
}
for (let i in addToMap) {
addToMap[i].updateFeatures(redraw);
}
}
function onPointermove(evt) {
//clearTimeout(pointerMoveTimeout);
//pointerMoveTimeout = setTimeout(highlight(evt), 100);
highlight(evt);
}
function highlight(evt) {
let evtCoords = evt.map.getCoordinateFromPixel(evt.pixel);
let source = webgl ? webglFeatures : PlaneIconFeatures;
let feature = source.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 * 20;
if (a**2 + b**2 > c**2) {
feature = null;
}
}
if (!feature) {
HighlightedPlane = null;
refreshHighlighted();
return;
}
const hex = feature.hex;
const values = feature.values_;
const mmsi = values ? values.mmsi : null;
if (hex) {
//console.log(hex);
}
if (mmsi) {
//console.log(mmsi);
}
if (HighlightedPlane && hex == HighlightedPlane.icao)
return;
//clearTimeout(pointerMoveTimeout);
if (hex) {
HighlightedPlane = g.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 = g.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);
}
}
lastRenderExtent = getRenderExtent();
if (urlIcaos.length > 0) {
const icaos = urlIcaos;
if (!usp.has('noIsolation') && !usp.has('replay'))
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 = g.planes[icao] || new PlaneObject(icao);
newPlane.last_message_time = NaN;
newPlane.position_time = NaN;
newPlane.selected = true;
select(newPlane, options);
(!zoom) && (zoom = 5);
} else {
(!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) {
db.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 (db.regCache) {
if (db.regCache[upper]) {
selectPlaneByHex(db.regCache[upper].toLowerCase(), {noDeselect: true, follow: true});
}
} else if (!regIcaoDownloadRunning) {
let req = regIcaoDownload({ upper: `${upper}` });
req.done(function() {
if (db.regCache[this.opts.upper]) {
selectPlaneByHex(db.regCache[this.opts.upper].toLowerCase(), {noDeselect: true, follow: true});
}
});
}
}
for (let i in g.planesOrdered) {
const plane = g.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))
) {
results.push(plane);
/* leaving this code in place just in case, not sure what this limitation to planes on screen is for when searching
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 (haveTraces) {
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 g.planesOrdered) {
g.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 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;
let updateAddressBarString = "";
function updateAddressBar() {
if (!window.history || !window.history.replaceState)
return;
if (heatmap || (pTracks && !haveTraces) || !CenterLat || uuid)
return;
let string = '';
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 (SelPlanes.length > 0) {
string += (string ? '&' : '?');
string += 'icao=' + SelPlanes.map((s) => encodeURIComponent(s.icao)).join(',')
}
if (showTrace || replay) {
string += (string ? '&' : '?');
string += 'lat=' + CenterLat.toFixed(3) + '&lon=' + CenterLon.toFixed(3) + '&zoom=' + g.zoomLvl.toFixed(1);
}
if (SelPlanes.length > 0 && (showTrace)) {
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 (labelsGeom) {
string += '&labelsGeom';
}
if (geomUseEGM) {
string += '&geomEGM';
}
}
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);
}
for (const filter of filters_active) {
filterStrings.push(`filter${filter.key}=${encodeURIComponent(filter.pattern)}`);
}
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 (string == updateAddressBarString) {
return;
}
updateAddressBarString = string;
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);
const latDiff = extent.maxLat - extent.minLat;
const lonDiff = extent.maxLon - extent.minLon;
const area = latDiff * lonDiff;
const areaThreshold = 30 * 30;
let min = 1;
let max = 7;
if (area > areaThreshold && !onlySelected) {
const factor2 = Math.min(4, (latDiff * lonDiff) / areaThreshold);
min *= factor2;
}
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();
const base = 70;
if (inactive < base)
inactive = base;
if (inactive > 4 * base)
inactive = 4 * base;
if (globeIndex) {
refresh *= inactive / base;
}
if (!mapIsVisible)
refresh *= 2;
if (aggregator && window.self != window.top) {
refresh *= 1.5;
} else if (onMobile && TrackedAircraftPositions > 800) {
refresh *= 1.5;
}
if (document.visibilityState === 'hidden') { refresh *= 4; } // in case visibility change events don't work, reduce refresh rate if visibilityState works
//console.log(refresh);
return refresh;
}
function toggleShowTrace() {
showTrace = !showTrace;
if (showTrace) {
jQuery("#selected_showTrace_hide").hide();
toggleFollow(false);
showTraceWasIsolation = onlySelected;
toggleIsolation("on", "noRefresh");
shiftTrace();
refreshFilter();
} else {
jQuery("#selected_showTrace_hide").show();
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: g.zoomLvl,});
if (replay) {
replayStep();
}
}
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 = 0; 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);
traceDate.setUTCMilliseconds(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: g.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) {
if (SiteOverride) {
return;
}
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,
};
console.log("watching position");
watchPositionId = navigator.geolocation.watchPosition(function(position) {
onLocationChange(position);
pollPositionSeconds = 60;
}, logArg, geoposOptions);
pollPositionInterval();
}
let geoFindInterval = null;
function geoFindMe() {
//console.trace();
g.geoFindDefer = jQuery.Deferred();
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';
}
initSitePos();
console.log('Location from browser: '+ SiteLat +', ' + SiteLon);
g.geoFindDefer.resolve();
{
// 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 ,
};
console.log('geoFindInterval: querying position');
navigator.geolocation.getCurrentPosition(onLocationChange, logArg, geoposOptions);
}, 15 * 60 * 1000);
}
}
function error() {
console.log("Unable to query location.");
initSitePos();
g.geoFindDefer.reject();
}
if (!geoFindEnabled()) {
//console.log('Geolocation is not enabled');
initSitePos();
g.geoFindDefer.reject();
} else if (!navigator.geolocation) {
console.log('Geolocation is not supported by your browser');
initSitePos();
g.geoFindDefer.reject();
} else {
// change SitePos on location change
console.log('Locating…');
const geoposOptions = {
enableHighAccuracy: false,
timeout: Infinity,
maximumAge: 300 * 1000,
};
navigator.geolocation.getCurrentPosition(success, error, geoposOptions);
}
return g.geoFindDefer;
}
let initSitePosFirstRun = true;
function initSitePos() {
// fall back to loStore position
if (loStore['SiteLat'] != null && loStore['SiteLon'] != null && SiteLat == null && SiteLon == null) {
SiteLat = CenterLat = DefaultCenterLat = parseFloat(loStore['SiteLat']);
SiteLon = CenterLon = DefaultCenterLon = parseFloat(loStore['SiteLon']);
}
// Set SitePosition
if (SiteLat != null && SiteLon != null) {
SitePosition = [SiteLon, SiteLat];
// Add home marker if requested
drawSiteCircle();
createLocationDot();
} else {
TAR.planeMan.setColumnVis('sitedist', false);
}
if (initSitePosFirstRun) {
initSitePosFirstRun = false;
const sortBy = usp.get('sortBy');
if (sortBy) {
TAR.planeMan.ascending = true;
TAR.planeMan.cols[sortBy].sort();
if (usp.has('sortByReverse')) {
TAR.planeMan.cols[sortBy].sort();
}
} else if (loStore['sortCol']) {
TAR.planeMan.sortAscending = Boolean(loStore['sortAscending']);
TAR.planeMan.cols[loStore['sortCol']].sort();
} else {
if (SitePosition) {
TAR.planeMan.cols.sitedist.sort();
} else {
TAR.planeMan.sortAscending = false;
TAR.planeMan.cols.altitude.sort();
}
}
}
}
/*
function drawAlt() {
processAircraft({hex: 'c0ffee', });
let plane = g.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 g.planesOrdered) {
const plane = g.planesOrdered[i];
plane.removeTrail();
plane.linesDrawn && (plane.drawLine = 1);
plane.updateFeatures();
}
}
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() {
//console.trace();
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 = 'hsla(' + colorArr[0].toFixed(0) + ',' + colorArr[1].toFixed(0) + '%,' + colorArr[2].toFixed(0) + '%,' + range_outline_alpha + ')';
}
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);
}
}
}
function drawOutlineJson() {
let request = jQuery.ajax({ url: actualOutline.url,
cache: false,
timeout: actualOutline.refresh,
dataType: 'json' });
request.done(function(data) {
actualOutline.features.clear();
let points = [];
if (data.multiRange) {
points = data.multiRange
} else if (data.actualRange && data.actualRange.last24h) {
points[0] = data.actualRange.last24h.points;
} else {
points[0] = data.points;
}
if (!points[0] || !points[0].length)
return;
for (let p = 0; p < points.length; ++p) {
let geom = null;
let lastLon = null;
for (let j = 0; j < points[p].length + 1; ++j) {
const k = j % points[p].length;
const lat = points[p][k][0];
const lon = points[p][k][1];
const proj = ol.proj.fromLonLat([lon, lat]);
if (!geom || (lastLon && Math.abs(lon - lastLon) > 270)) {
geom = new ol.geom.LineString([proj]);
actualOutline.features.addFeature(new ol.Feature(geom));
} else {
geom.appendCoordinate(proj);
}
lastLon = lon;
}
}
});
request.fail(function() {
// no rings available, do nothing
});
}
function gotoTime(timestamp) {
//console.log(`gotoTime(${timestamp}) animate: {${traceOpts.animate}}`);
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;
}
traceOpts.animateStepTime = traceOpts.animateRealtime / traceOpts.replaySpeed / traceOpts.animateSteps;
clearTimeout(traceOpts.showTimeout);
//console.log(`setTimeout(gotoTime, (${traceOpts.animateStepTime}))`);
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 = g.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();
let refDate = (replay ? replay.ts : traceDate) || today;
if ((showTrace || replay) && !(today.getTime() > refDate.getTime() && today.getTime() < refDate.getTime() + (24 * 3600 + 60 * 60) * 1000)) {
URL1 = null;
URL2 = 'globe_history/' + zDateString(refDate).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) {
URL2 = 'globe_history/' + zDateString(refDate).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 = g.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 = g.planes[options.plane];
plane.fullTrace = normalizeTraceStamps(data);
options.defer.done(function(options) {
const plane = g.planes[options.plane];
if (showTrace) {
legShift(0, plane);
if (!multiSelect && showTraceTimestamp) {
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 = g.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 pointsU = new Uint32Array(points.buffer);
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 if (alt == -124)
alt = null;
else
alt *= 25;
let gs = points[i + 3] >> 16;
if (gs == -1)
gs = null;
else
gs /= 10;
if (PlaneFilter.enabled && altFiltered(alt))
continue;
if (heatmap.filters) {
let type = (pointsU[i] >> 27) & 0x1F;
let dataSource;
switch (type) {
case 0: dataSource = 'adsb'; break;
case 1: dataSource = 'modeS'; break;
case 2: dataSource = 'adsr'; break;
case 3: dataSource = 'tisb'; break;
case 4: dataSource = 'adsc'; break;
case 5: dataSource = 'mlat'; break;
case 6: dataSource = 'other'; break;
case 7: dataSource = 'modeS'; break;
case 8: dataSource = 'adsb'; break;
case 9: dataSource = 'adsr'; break;
case 10: dataSource = 'tisb'; break;
case 11: dataSource = 'tisb'; break;
default: dataSource = 'unknown';
}
let hex = (pointsU[i] & 0xFFFFFF).toString(16).padStart(6, '0');
hex = (pointsU[i] & 0x1000000) ? ('~' + hex) : hex;
let plane = g.planes[hex] || new PlaneObject(hex);
plane.dataSource = dataSource;
if (plane.isFiltered()) {
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").hide();
}
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");
let playing = true;
let speed = 30;
if (usp.has("replaySpeed")) {
speed = usp.getFloat("replaySpeed");
}
if (speed == 0) {
speed = 30;
playing = false;
}
if (usp.has("replayPaused")) {
playing = false;
}
return {
playing: playing,
ts: ts,
ival: 60 * 1000,
speed: speed,
dateText: zDateString(ts),
hours: ts.getUTCHours(),
minutes: ts.getUTCMinutes(),
};
}
function replayClear() {
clearTimeout(refreshId);
reaper(true);
refreshFilter();
replayPlanes = {};
}
function replayGetChunk(ts) {
let time = new Date(ts);
let sDate = sDateString(time);
let index = 2 * time.getUTCHours() + Math.floor(time.getUTCMinutes() / 30);
let key = `${sDate} chunk ${index}`;
let url = "globe_history/" + sDate + "/heatmap/" + index.toString().padStart(2, '0') + ".bin.ttf";
return { date: sDate, index: index, key: key, url: url, };
}
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();
if (!g.replayCache) {
g.replayCache = new ItemCache(onMobile ? 12 : 24);
}
let chunk = replayGetChunk(ts);
let rKey = chunk.key;
let data = g.replayCache.get(rKey);
if (data) {
initReplay(chunk, data);
} else {
if (rKey == replay.loadingKey) {
// console.log('if (rKey == replay.loadingKey) {');
// download already in progress, do nothing
return;
}
if (replay.abortController) {
replay.abortController.abort();
}
replay.loadingKey = rKey;
jQuery('#replayLoading').text('Loading ...');
replay.abortController = new AbortController();
jQuery("#update_error").css('display','none');
clearTimeout(replay.errorTimeout);
const ff = () => {
//console.log(`finally ${rKey}`);
delete replay.loadingKey;
jQuery('#replayLoading').text('');
};
const errorFunc = (error) => {
ff();
if (error.name == 'AbortError') {
//console.log(`aborted: ${rKey}`);
return;
}
jQuery("#update_error_detail").text(error.message + ' --> No data for this timestamp!');
jQuery("#update_error").css('display','block');
replay.errorTimeout = setTimeout(() => { jQuery("#update_error").css('display','none'); }, 5000);
};
fetch(chunk.url, { signal: replay.abortController.signal })
.then(
(response) => {
if (!response.ok) {
throw new Error(`HTTP error, status = ${response.status}`);
}
response.arrayBuffer()
.then((data) => {
delete replay.abortController;
g.replayCache.add(rKey, data);
initReplay(chunk, data);
//console.log(`loaded: ${rKey}`);
ff();
})
.catch(errorFunc)
;
},
errorFunc
)
.catch( errorFunc )
;
}
}
function initReplay(chunk, data) {
if (data.byteLength % 16 != 0) {
console.log("Invalid heatmap file (byteLength)");
return;
}
replay.loadedKey = chunk.key;
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 setReplayTimeHint(date) {
if (true || utcTimesHistoric) {
jQuery("#replayDateHintLocal").html(TIMEZONE + " Date: " + lDateString(date));
jQuery("#replayDateHint").html("" + zDateString(date));
jQuery("#replayTimeHint").html("UTC:" + NBSP + zuluTime(date) + ' / ' + TIMEZONE + ":" + NBSP + localTime(date));
} else {
jQuery("#replayDateHintLocal").html("");
jQuery("#replayDateHint").html("Date: " + lDateString(date));
jQuery("#replayTimeHint").html("Time: " + localTime(date) + NBSP + TIMEZONE);
}
}
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));
setReplayTimeHint(date);
}
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;
dateString = zDateString(replay.ts);
timeString = zuluTime(replay.ts) + NBSP + 'Z';
setReplayTimeHint(replay.ts);
if (replay.datepickerDate != dateString) {
replay.datepickerDate = dateString;
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.loadedKey != replayGetChunk(replay.ts).key) {
console.error('no data loaded for current timestamp, can\'t play!');
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;
g.now = now;
traceOpts.endStamp = now + replay.ival;
replay.ival = (replay.pointsU[i + 3] & 65535) / 1000;
if (arg != 'fast') {
replaySetTimeHint();
if (replay.addressMinutes != replay.minutes) {
replay.addressMinutes = replay.minutes;
updateAddressBar();
}
//console.log(replay.ts.toUTCString());
if (now - lastReap > 60) {
reaper();
}
}
i += 4;
let ext;
if (g.zoomLvl > 10) {
ext = currentExtent(1.6);
} else if (g.zoomLvl > 8) {
ext = currentExtent(1.2);
} else {
ext = currentExtent(1.1);
}
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 hex = (points[i] & ((1<<24) - 1)).toString(16).padStart(6, '0');
hex = (points[i] & (1<<24)) ? ('~' + hex) : hex;
let ac = {hex:hex, seen:0, seen_pos:0,};
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');
if (!g.planes[hex]) {
replayPlanes[hex] = ac;
continue;
}
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 if (alt == -124)
alt = null;
else
alt *= 25;
let gs = points[i + 3] >> 16;
if (gs == -1)
gs = null;
else
gs /= 10;
let ac = {
seen: 0,
seen_pos: 0,
};
if (!g.planes[hex]) {
const cached = replayPlanes[hex];
if (cached) {
ac = cached;
//delete replayPlanes[hex];
}
}
ac.hex = hex;
ac.lat = lat;
ac.lon = lon;
ac.alt_baro = alt;
ac.gs = gs;
ac.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.messageRate && data.messageRate > 0) {
MessageRate = data.messageRate;
} else 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) {
// .. and clean up any old values
while ((now - MessageCountHistory[0].time) > 10.5) {
MessageCountHistory.shift();
}
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;
}
//console.log(message_time_delta);
}
} 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) {
if (replay.loadedKey != replayGetChunk(replay.ts).key) {
console.error('no data loaded for current timestamp, can\'t play!');
return;
}
replay.playing = true;
jQuery("#replayPlay").html("Pause");
replayStep();
} else {
replay.playing = false;
jQuery("#replayPlay").html("Play");
clearTimeout(refreshId);
}
};
function showReplayBar(){
console.log('showReplayBar()');
showingReplayBar = !showingReplayBar;
if (!showingReplayBar){
jQuery("#replayBar").hide();
replay = null;
jQuery('#map_canvas').height('100%');
jQuery('#sidebar_canvas').height('100%');
jQuery("#selected_showTrace_hide").show();
} else {
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);
let datepickerOptions = {
maxDate: '+1d',
dateFormat: "yy-mm-dd",
autoSize: true,
onSelect: function(dateText) {
replay.dateText = dateText;
replayJump();
},
};
if (onMobile) {
datepickerOptions.onClose = function(dateText, inst){
jQuery("replayDatepicker").attr("disabled", false);
};
datepickerOptions.beforeShow = function(input, inst){
jQuery("replayDatepicker").attr("disabled", true);
};
} else {
//
}
jQuery("#replayDatepicker").datepicker(datepickerOptions);
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(1000, 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');
jQuery("#selected_showTrace_hide").hide();
}
};
function timeoutFetch() {
console.log("timeoutFetch " + localTime(new Date()));
fetchData();
if (timers.timeoutFetch) {
clearTimeout(timers.checkMove);
}
timers.timeoutFetch = setTimeout(timeoutFetch, Math.max(RefreshInterval, 10000));
if (now - lastReap > 120) {
reaper();
}
}
function refreshHistory() {
if (heatmap || replay || globeIndex || pTracks || uuid || !HistoryChunks) {
noLongerHidden();
return;
}
if (1 && (new Date().getTime() - g.hideStamp) / 1000 < 5) {
console.log('short tab change, not loading history');
noLongerHidden();
return;
}
jQuery("#loader_progress").attr('value', 0);
setTimeout(() => {
if (!timersActive) {
jQuery("#loader").show();
}
}, 200);
chunksDefer().done(function(data) {
console.log(localTime(new Date()) + ' tab change, loading history');
g.refreshHistory = true;
HistoryChunks = true;
chunkNames = [];
jQuery("#loader_progress").attr('value', 1);
try {
for (let i = data.chunks.length-1; i >= 0; i--) {
let f = data.chunks[i];
chunkNames.push(f);
// break after we found a chunk that's older than now
// chunk timestamp is the start of its data, not the end
// so we need to include the first chunk that's older
// which is done above
let parts = f.split(".")[0].split("_");
if (parts[0] == "chunk") {
if (now > parts[1]/1e3) {
break;
}
}
}
//console.log(chunkNames);
nHistoryItems = chunkNames.length;
get_history();
push_history();
} catch (e) {
console.error(e);
noLongerHidden();
}
}).fail(function() {
setTimeout(refreshHistory, 500);
});
}
function handleVisibilityChange() {
const prevHidden = tabHidden;
if (document[hideName])
tabHidden = true;
else
tabHidden = false;
if (tabHidden && timersActive) {
g.hideStamp = new Date().getTime();
clearIntervalTimers();
if (!globeIndex) {
//timeoutFetch();
}
replay_was_active = replay.playing;
if (replay.playing) {
playReplay(false);
}
}
// tab is no longer hidden
if (!tabHidden && !timersActive) {
loadFinished && jQuery("#timers_paused").css('display','none');
globeRateUpdate();
if (heatmap || replay || globeIndex || pTracks) {
noLongerHidden();
} else {
refreshHistory();
}
}
}
function noLongerHidden() {
active();
setIntervalTimers();
jQuery("#loader").hide();
refresh();
if (replay_was_active) {
playReplay(true);
}
if (showTrace)
return;
if (heatmap)
return;
if (!haveTraces)
return;
let count = 0;
if (multiSelect && !SelectedAllPlanes) {
for (let i = 0; i < g.planesOrdered.length; ++i) {
let plane = g.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 autoSelectClosest() {
if (!loadFinished)
return;
let closest = null;
let closestDistance = null;
checkMovement();
for (let key in g.planesOrdered) {
const plane = g.planesOrdered[key];
if (!plane.visible)
continue;
if (!closest)
closest = plane;
if (plane.position == null || plane.seen_pos > 20)
continue;
let refLoc = [CenterLon, CenterLat];
if (autoselectCoords && autoselectCoords.length == 2) {
refLoc = [ autoselectCoords[1], autoselectCoords[0] ];
}
const dist = ol.sphere.getDistance(refLoc, 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(autoSelectClosest, 5000);
autoSelectClosest();
}
function registrationLink(plane) {
if (plane.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".
let RGBColorToKMLColor = function(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 = '\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 g.planesOrdered) {
let plane = g.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 (!disableGeoLocation && !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);
}
function copyShareLink() {
navigator.clipboard.writeText(shareLink);
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 = "Hex:" + NBSP + selected.icao.toUpperCase() + "";
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 = "" + copy_link_text + "";
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')) {
layerDimFactor = 0.25;
} else if (MapType_tar1090 == 'esri') {
layerDimFactor = 0.5;
} else if (MapType_tar1090 == 'gibs') {
layerDimFactor = 0.5;
} else if (MapType_tar1090.startsWith('carto_raster')) {
layerDimFactor = 0.70;
layerExtraContrast = 0.6;
} else if (MapType_tar1090.startsWith('carto_light')) {
layerDimFactor = 0.80;
layerExtraContrast = 0.2;
} else if (MapType_tar1090.startsWith('carto_dark')) {
layerDimFactor = 0.25;
layerExtraContrast = 0.05;
} else {
layerDimFactor = 1;
layerExtraContrast = 0;
}
}
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 getRenderExtent(extra) {
extra || (extra = 0);
const mapSize = OLMap.getSize();
const over = renderBuffer + extra;
const size = [mapSize[0] + over, mapSize[1] + over];
return myExtent(OLMap.getView().calculateExtent(size));
}
function requestBoxString() {
if (!mapIsVisible && lastRequestBox) {
return lastRequestBox;
}
let extent = getRenderExtent(80);
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 (aggregator && window.location.hostname.startsWith('inaccurate')) {
jQuery('#inaccurate_warning').removeClass('hidden');
document.getElementById('inaccurate_warning').innerHTML = `
This map includes inaccurate / very approximate positions, errors of 200 nmi or more are not unusual.
If no ADS-B / MLAT info is available but at least 1 receiver is receiving ModeS data from a hex, the aircraft is placed where the receiving station on average receives planes which do have a location.
Please add a disclaimer to any screenshots of this website or better yet just report that an aircraft was spotted in the approximate area WITHOUT using screenshots.
`;
}
function getn(n) {
limitUpdates=n; RefreshInterval=0; fetchCalls=0;
}
function onAltimeterSetStandard(e) {
e.preventDefault();
jQuery("#altimeter_input").val(1013.25);
onAltimeterChange(e);
}
function onAltimeterSetSelected(e) {
e.preventDefault();
if (!SelectedPlane || !SelectedPlane.nav_qnh) {
return;
}
jQuery("#altimeter_input").val(SelectedPlane.nav_qnh);
onAltimeterChange(e);
}
function onAltimeterChange(e) {
e.preventDefault();
jQuery("#altimeter_input").blur();
let altimeter = parseFloat(jQuery("#altimeter_input").val().trim());
if (altimeter < 100) {
// assume inHg, convert to mbar
baroCorrectQNH = 33.8639 * altimeter;
} else {
// assume mbar / hPa
baroCorrectQNH = altimeter;
}
remakeTrails();
refreshSelected();
refreshFeatures();
TAR.planeMan.redraw();
refresh();
}
// Using formula from: https://www.weather.gov/media/epz/wxcalc/pressureAltitude.pdf
// See also: https://en.wikipedia.org/wiki/Pressure_altitude
// Inverse equation on wikipedia seems imprecise,
// used the the weather.gov pdf and inverted the equation myself
// This uses ISA atmosphere (should be the same as altimeters in planes)
function adjust_baro_alt(alt) {
if (!baroUseQNH || alt == null || alt == "ground") {
return alt;
}
let station_pressure = Math.pow(1 - alt / 145366.45, 5.2553026) * 1013.25;
let res = ( 1 - Math.pow(station_pressure / baroCorrectQNH, 0.190284) ) * 145366.45;
return res;
}
function globeRateUpdate() {
if (aggregator) {
dynGlobeRate = true;
if (0) {
const cookieExp = getCookie('asdf_id').split('_')[0];
const ts = new Date().getTime();
if (!cookieExp || cookieExp < ts + 3600*1000)
setCookie('adsbx_sid', ((ts + 2*86400*1000) + '_' + Math.random().toString(36).substring(2, 15)), 2);
}
}
if (dynGlobeRate) {
return jQuery.ajax({url:'/globeRates.json', cache: false, dataType: 'json', }).done(function(data) {
if (data.simload != null)
globeSimLoad = data.simload;
if (data.refresh != null && globeIndex)
RefreshInterval = data.refresh;
});
} else {
return jQuery.Deferred().resolve();
}
}
globeRateUpdate();
parseURLIcaos();
initialize();