// Some global variables are defined in early.js // early.js takes care of getting some history files while the html page and // some javascript libraries are still loading, hopefully speeding up loading "use strict"; g.planes = {}; g.planesOrdered = []; g.route_cache = []; g.route_check_array = []; g.route_check_in_flight = false; g.route_cache_timer = new Date().getTime() / 1000 + 1; // one second from now // Define our global variables let tabHidden = false; let webgl = false; let webglFeatures = new ol.source.Vector(); let webglLayer; let OLMap = null; let OLProj = null; let OLProjExtent = null; let PlaneIconFeatures = new ol.source.Vector(); let trailGroup = new ol.Collection(); let siteCircleLayer; let siteCircleFeatures = new ol.source.Vector(); let locationDotLayer; let locationDotFeatures = new ol.source.Vector(); let iconLayer; let trailLayers; let heatFeatures = []; let heatFeaturesSpread = 1024; let heatLayers = []; let realHeatFeatures = new ol.source.Vector(); let realHeat; let iconCache = {}; let addToIconCache = []; let lineStyleCache = {}; let replayPlanes = {}; let PlaneFilter = {}; let SelectedPlane = null; let sp = null; let SelPlanes = []; let SelectedAllPlanes = false; let HighlightedPlane = null; let FollowSelected = false; let followPos = []; let loadFinished = false; let loadStart = new Date().getTime(); let mapResizeTimeout; let pointerMoveTimeout; let iconSize = 1; let debugTracks = false; let debugAll = false; let trackLabels = false; let grouptype_checkbox; let multiSelect = false; let uat_data = null; let enableLabels = false; let extendedLabels = 0; let mapIsVisible = true; let onlyMilitary = false; let onlySelected = false; let debug = false; let debugJump = false; let jumpTo = null; let noMLAT = false; let noVanish = false; let filterTracks = false; // altitude filter: don't filter planes but rather their tracks by altitude let refreshId = 0; let lastFetch = 0; let globeIndexNow = {}; let globeIndexDist = {}; let globeIndexSpecialLookup = {}; let globeTilesViewCount = 0; let globeTableLimitBase = 80; let globeTableLimit = 80; let fetchCounter = 0; let lastGlobeExtent; let lastRenderExtent; let pendingFetches = 0; let firstFetch = true; let debugCounter = 0; let pathName = window.location.pathname.replace(/\/+/, '/') || "/"; let icaoFilter = null; let sourcesFilter = null; let sources = ['adsb', ['uat', 'adsr'], 'mlat', 'tisb', 'modeS', 'other', 'adsc']; let flagFilter = null; let flagFilterValues = ['military', 'pia', 'ladd']; let showTrace = false; let showTraceExit = false; let showTraceWasIsolation = false; let showTraceTimestamp = null; let traceDate = null; let traceDateString = null; let traceOpts = {}; let icaoParam = null; let globalScale = 1; let userScale = 1; let iconScale = 1; let labelScale = 1; let newWidth = lineWidth; let SiteOverride = false; let onJumpInput = null; let labelFill = null; let blackFill = null; let labelStroke = null; let labelStrokeNarrow = null; let bgFill = null; let legSel = -1; let geoMag = null; let solidT = false; let lastActive = new Date().getTime(); let enableOverlays = []; let halloween = false; let noRegOnly = false; let triggerRefresh = 0; let firstDraw = true; let darkerColors = false; let autoselect = false; let nogpsOnly = false; let spritesDataURL = null; let trace_hist_only = false; let traces_high_res = false; let show_rId = true; let labels_top = false; let lockDotCentered = false; let overrideMapType = null; let layerMoreContrast = false; let layerExtraDim = 0; let layerExtraContrast = 0; let shareFiltersParam = false; let lastRequestSize = 0; let lastRequestBox = ''; let nextQuerySelected = 0; let enableDynamicCachebusting = false; let lastRefreshInt = 1000; let limitUpdates = -1; let infoBlockWidth = baseInfoBlockWidth; const renderBuffer = 60; let shareLink = ''; let CenterLat = 0; let CenterLon = 0; let zoomLvl = 5; let zoomLvlCache; let TrackedAircraft = 0; let globeTrackedAircraft = 0; let TrackedAircraftPositions = 0; let TrackedHistorySize = 0; let aircraftShown = 0; let SitePosition = null; // timestamps let now = 0; let last = 0; let uat_now = 0; let uat_last = 0; let FetchPending = []; let FetchPendingUAT = null; let MessageCountHistory = []; let MessageRate = 0; let layers; let layers_group; const nullStyle = new ol.style.Style({}); let estimateStyle; let estimateStyleSlim; let badLine; let badLineMlat; let badDot; let badDotMlat; let showingReplayBar = false; function processAircraft(ac, init, uat) { let isArray = Array.isArray(ac); let hex = isArray ? ac[0] : ac.hex; if (icaoFilter && !icaoFilter.includes(hex)) return; if (uat && uatNoTISB && ac.type && ac.type.substring(0,4) == "tisb") { // drop non ADS-B planes from UAT (TIS-B) return; } // Do we already have this plane object in g.planes? // If not make it. let plane = g.planes[hex] if (!plane) { plane = new PlaneObject(hex); if (uat) plane.uat = true; } if (showTrace) return; // Call the function update if (globeIndex || replay) { if (!onlyMilitary || plane.military || (ac.dbFlags && ac.dbFlags & 1)) { plane.updateData(now, last, ac, init); } else { plane.last_message_time = now - ac.seen; } return; } if (uat_now == 0) { plane.updateData(now, last, ac, init); return; } // UAT dual source stuff below let newPos = ac.seen_pos; if (newPos === undefined || isNaN(newPos)) { newPos = 1000; } let oldPos = plane.seen_pos; if (oldPos === undefined || isNaN(oldPos)) { oldPos = 1000; } let newSeen = ac.seen; if (newSeen === undefined || isNaN(newSeen)) { newSeen = 1000; } let oldSeen = plane.seen; if (oldSeen === undefined || isNaN(oldSeen)) { oldSeen = 1000; } if (!uat) { if (!plane.uat // plane isn't uat, new data isn't uat, accept immediately || (prefer978 < 0 && newPos < -prefer978) || (newPos < 2 && oldPos > 4 + prefer978) || (oldPos > 60 && newSeen < 2 && oldSeen > 4 + prefer978) || init) { plane.uat = false; plane.updateData(now, last, ac, init); } } else { if (plane.uat // plane is uat, new data is uat, accept immediately || (prefer978 > 0 && newPos < prefer978) || (newPos < 2 && (oldPos > 4 - prefer978 || plane.dataSource == "mlat")) || (oldPos > 60 && newSeen < 2 && oldSeen > 4 - prefer978) || init) { let tisb = Array.isArray(ac) ? (ac[7] == "tisb") : (ac.tisb != null && ac.tisb.indexOf("lat") >= 0); if (tisb && plane.dataSource == "adsb") { // ignore TIS-B data for current ADS-B 1090 planes } else { plane.uat = true; plane.updateData(uat_now, uat_last, ac, init); } } } } let notNewCounter = 0; let backwardsCounter = 0; function processReceiverUpdate(data, init) { // update now and last let uat = data.uat_978; if (uat) { if (data.now <= uat_now) return; uat_last = uat_now; uat_now = data.now; } else { if (uat_now && now - uat_now > 15) { uat_now = now; } if (data.now <= now && !globeIndex) { if (data.now < now) { backwardsCounter++; console.log('timestep backwards or the same, ignoring data:' + now + ' -> ' + data.now); if (backwardsCounter >= 5) { backwardsCounter = 0; console.log('resetting all data now:' + now + ' -> ' + data.now); now = data.now; last = now - 1; reaper(1); enableDynamicCachebusting = true; } } if (++notNewCounter > 2) { // data.now is too old, possibly a caching issues enableDynamicCachebusting = true; } return; } if (data.now > now) { notNewCounter = 0; backwardsCounter = 0; last = now; now = data.now; } } if (globeIndex) { if ((showGrid || loStore['globeGrid'] == 'true') && globeIndexNow[data.globeIndex] == null) drawTileBorder(data); globeTrackedAircraft = data.global_ac_count_withpos; globeIndexNow[data.globeIndex] = data.now; } if (!(uat || init || (globeIndex && adsbexchange))) { updateMessageRate(data); } // Loop through all the planes in the data packet for (let j=0; j < data.aircraft.length; j++) { processAircraft(data.aircraft[j], init, uat); } } function fetchFail(jqxhr, status, error) { try { pendingFetches--; if (pendingFetches <= 0 && !tabHidden) { triggerRefresh++; checkMovement(); } status = jqxhr.status; if (jqxhr.readyState == 0) error = "Can't connect to server, check your network!"; let errText = status + (error ? (": " + error) : ""); console.log(jqxhr); console.log(error); if (status != 429 && status != '429') { jQuery("#update_error_detail").text(errText); jQuery("#update_error").css('display','block'); StaleReceiverCount++; } else { if (C429++ > 16) { globeRateUpdate(); C429 = 0; } } } catch (e) { console.error(e); } } function fetchDone(data) { try { pendingFetches--; if (data == null) { return; } if (zstd) { let arr = new Uint8Array(data); lastRequestSize = arr.byteLength; let res; try { res = zstdDecode( arr, 0 ); } catch (e) { let errText = e.message; console.log(errText); jQuery("#update_error_detail").text(errText); jQuery("#update_error").css('display','block'); return; } let arrayBuffer = res.buffer // return type is Uint8Array, wqi requires the ArrayBuffer data = { buffer: arrayBuffer, }; wqi(data); } else if (binCraft) { lastRequestSize = data.byteLength / 2; data = { buffer: data, }; wqi(data); } data.urlIndex = this.urlIndex; if (!data.aircraft || !data.now) { let error = data.error; if (error) { jQuery("#update_error_detail").text(error); jQuery("#update_error").css('display','block'); StaleReceiverCount++; } return; } //console.time("Process " + data.globeIndex); processReceiverUpdate(data); //console.timeEnd("Process " + data.globeIndex); data = null; if (uat_data) { processReceiverUpdate(uat_data); uat_data = null; } if (pendingFetches <= 0 && !tabHidden) { triggerRefresh++; checkMovement(); if (firstFetch) { firstFetch = false; if (uuid) { const ext = myExtent(OLMap.getView().calculateExtent(OLMap.getSize())); let jump = true; for (let i = 0; i < g.planesOrdered.length; ++i) { const plane = g.planesOrdered[i]; if (plane.visible && inView(plane.position, ext)) { jump = false; break; } } if (jump) { followRandomPlane(); deselectAllPlanes(); OLMap.getView().setZoom(6); } } checkRefresh(); } } if (fetchCalls == 1) { console.timeEnd("first fetch()"); }; if (!g.firstFetchDone) { afterFirstFetch(); }; // Check for stale receiver data if (last == now && !globeIndex) { StaleReceiverCount++; if (StaleReceiverCount > 5) { jQuery("#update_error_detail").text("The data from the server hasn't been updated in a while."); jQuery("#update_error").css('display','block'); } } else if (StaleReceiverCount > 0){ StaleReceiverCount = 0; jQuery("#update_error").css('display','none'); } } catch (e) { console.error(e); } } function db_load_type_cache() { jQuery.getJSON(databaseFolder + "/icao_aircraft_types2.js").done(function(typeLookupData) { g.type_cache = typeLookupData; for (let i in g.planesOrdered) { g.planesOrdered[i].setTypeData(); } refresh(); }); } g.afterLoad = []; function runAfterLoad(func) { if (g.firstFetchDone) { func() } else { g.afterLoad.push(func); } } function afterFirstFetch() { if (g.firstFetchDone) { return; } g.firstFetchDone = true; setTimeout(() => { console.time('afterFirstFetch'); let func; while ((func = g.afterLoad.pop())) { func(); } geoMag = geoMagFactory(cof2Obj()); db_load_type_cache(); // this will do a refresh() if (limitUpdates != 0) { (typeof load_gt != 'undefined') && (load_gt) && (load_gt()) && (load_gt = null); (typeof load_fi != 'undefined') && (load_fi) && (load_fi()) && (load_fi = null); (typeof load_freestar != 'undefined') && (load_freestar) && (load_freestar()) && (load_freestar = null); } else { (typeof hide_freestar != 'undefined') && (hide_freestar) && (hide_freestar()); } if (usp.has('screenshot')) { clearIntervalTimers('silent'); } console.timeEnd('afterFirstFetch'); }, 150); } let debugFetch = false; let C429 = 0; let fetchCalls = 0; function fetchData(options) { options = options || {}; if (heatmap || replay || showTrace || pTracks || !loadFinished || inhibitFetch) { return; } let currentTime = new Date().getTime(); if (!options.force) { if ( currentTime - lastFetch < refreshInt() || pendingFetches > 0 || OLMap.getView().getInteracting() || OLMap.getView().getAnimating() ) { return; } } if (debugFetch) { console.log('Time since last fetch: ' + (currentTime - lastFetch)/1000); } lastFetch = currentTime; FetchPending = []; if (FetchPendingUAT != null) { // don't double up on fetches, let the last one resolve return; } //console.timeEnd("Starting Fetch"); //console.time("Starting Fetch"); if (limitUpdates != -1 && fetchCalls > limitUpdates) { return; } fetchCalls++; if (fetchCalls == 1) { console.time("first fetch()"); }; if (enable_uat) { FetchPendingUAT = jQuery.ajax({ url: 'chunks/978.json', dataType: 'json' }); FetchPendingUAT.done(function(data) { uat_data = data; FetchPendingUAT = null; }); FetchPendingUAT.fail(function(jqxhr, status, error) { FetchPendingUAT = null; }); } let suffix = zstd ? '.binCraft.zst' : (binCraft ? '.binCraft' : '.json'); if (enableDynamicCachebusting) { suffix += `?${currentTime}`; } let ac_url = []; if (uuid != null) { for (let i in uuid) { ac_url.push('uuid/?feed=' + uuid[i]); } } else if (reApi) { let url = 're-api/?' + (binCraft ? 'binCraft' : 'json'); url += zstd ? '&zstd' : ''; url += onlyMilitary ? '&filter_mil' : ''; lastRequestBox = requestBoxString(); if (icaoFilter) { if (icaoFilter.length > 0) { url += '&find_hex=' for (let k in icaoFilter) { url += icaoFilter[k] + ',' } url = url.slice(0, -1); // remove trailing comma } } else if (onlySelected && SelPlanes.length > 0) { url += '&find_hex=' for (let k in SelPlanes) { url += SelPlanes[k].icao + ',' } url = url.slice(0, -1); // remove trailing comma } else { url += '&box=' + lastRequestBox; if (SelPlanes.length > 0) { url += '&find_hex=' for (let k in SelPlanes) { url += SelPlanes[k].icao + ',' } url = url.slice(0, -1); // remove trailing comma } } ac_url.push(url); //console.log(url); } else if (globeIndex) { let indexes = globeIndexes(); const ancient = (currentTime - 2 * refreshInt() / globeSimLoad * globeTilesViewCount) / 1000; for (let i in indexes) { const k = indexes[i]; if (globeIndexNow[k] < ancient) { globeIndexNow[k] = null; } } indexes.sort(function(x,y) { if (!globeIndexNow[x] && !globeIndexNow[y]) { return globeIndexDist[x] - globeIndexDist[y]; } if (globeIndexNow[x] == null) return -1; if (globeIndexNow[y] == null) return 1; return (globeIndexNow[x] - globeIndexNow[y]); }); if (binCraft && onlyMilitary && zoomLvl < 5.5) { ac_url.push('data/globeMil_42777' + suffix); } else { indexes = indexes.slice(0, globeSimLoad); let mid = (binCraft && onlyMilitary) ? 'Mil_' : '_'; for (let i in indexes) { ac_url.push('data/globe' + mid + indexes[i].toString().padStart(4, '0') + suffix); } } } else { ac_url.push('data/aircraft' + suffix); } pendingFetches += ac_url.length; fetchCounter += ac_url.length; for (let i in ac_url) { if (debugFetch) { console.log('Fetching: ' + ac_url[i]); } let req; if (binCraft || zstd) { req = jQuery.ajax({ url: `${ac_url[i]}`, method: 'GET', xhr: arraybufferRequest, timeout: 15000, urlIndex: i, }); } else { req = jQuery.ajax({ url: `${ac_url[i]}`, dataType: 'json', urlIndex: i }); } FetchPending.push(req); req .done(fetchDone) .fail(fetchFail); } if (now - lastReap > 60) { reaper(); } } // this function is called from index.html on body load // kicks off the whole rabbit hole function initialize() { if (usp.has('iconTest')) { jQuery('#iconTestCanvas').show(); iconTest(); return; } jQuery.when(configureReceiver, heatmapDefer).done(function() { if (receiverJson) { if (receiverJson.trace_hist_only) trace_hist_only = true; if (receiverJson.json_trace_interval < 2) traces_high_res = true; if (receiverJson.lat != null) { SiteLat = receiverJson.lat; SiteLon = receiverJson.lon; SitePosition = [SiteLon, SiteLat]; DefaultCenterLat = receiverJson.lat; DefaultCenterLon = receiverJson.lon; } if (receiverJson.jaeroTimeout) { jaeroTimeout = receiverJson.jaeroTimeout * 60; } if (receiverJson.readsb) { positionFilter = false; altitudeFilter = false; } } configureReceiver = null; // Initialize stuff initPage(); initMap(); processQueryToggles(); if (nHistoryItems) { // Wait for history item downloads and append them to the buffer push_history(); jQuery.when(historyLoaded).done(afterHistoryLoad); } else { afterHistoryLoad(); } }); } function afterHistoryLoad() { if (nHistoryItems) { reaper(); } if (!zstdDecode) { startPage(); } else { try { zstddec.promise.then(function() { startPage(); }); } catch (e) { webAssemblyFail(e); startPage(); } } } function processQueryToggles() { if (!usp.has('toggles')) { return; } let todo; try { todo = usp.get('toggles').split(',');; } catch (e) { console.error(e); return; } while (todo.length >= 2) { let key = todo.shift(); let value = todo.shift(); let state = true; if (value == 'false' || value == '0') { state = false; } try { toggles[key].toggle(state, "init"); console.log((state ? "Enabled" : "Disabled") + " setting: " + key); } catch (e) { console.error(e); } } } function replaySpeedChange(arg) { traceOpts.replaySpeed = arg; console.log(arg); if (traceOpts.animate) return; legShift(0); }; function initPage() { let value; if (uk_advisory) { labels_top = true; tempTrails = true; tempTrailsTimeout = 45; SiteCirclesDistances = new Array(5, 10, 20); SiteCirclesLineDash = [5, 5]; SiteCirclesColors = ['#2b3436', '#2b3436', '#2b3436']; defaultOverlays.push('uka_airports'); defaultOverlays.push('uka_airspaces'); defaultOverlays.push('uka_runways'); defaultOverlays.push('uka_shoreham'); MapType_tar1090 = 'carto_light_all'; lineWidth=4; enableLabels=true; } if (usp.has('debugFetch')) { debugFetch = true; } if (usp.has('rangeRings')) { SiteCircles = Boolean(parseInt(usp.get('rangeRings'))); } if (usp.has('limitUpdates')) { let tmp = parseInt(usp.get('limitUpdates')); if (!isNaN(tmp)) limitUpdates = tmp; } if (usp.has('screenshot')) { limitUpdates = 0; } if (usp.has('nowebgl')) { loStore['webgl'] = "false"; } if (usp.has('showGrid')) { showGrid = true; loStore['layer_site_pos'] = 'true'; } if (usp.has('halloween')) halloween = true; if (usp.has('outlineWidth')) { let tmp = parseInt(usp.get('outlineWidth')); if (!isNaN(tmp)) outlineWidth = tmp; } if (usp.has('kiosk')) { tempTrails = true; hideButtons = true; userScale = 2; } if (pTracks) { noVanish = true; buttonActive('#P', noVanish); filterTracks = true; selectAllPlanes(); } if (usp.has('mobile')) onMobile = true; if (usp.has('desktop')) onMobile = false; if (usp.has('hideSidebar')) loStore['sidebar_visible'] = "false"; if (usp.has('sidebarWidth')) { loStore['sidebar_width'] = usp.get('sidebarWidth'); loStore['sidebar_visible'] = "true"; } if (usp.has('SiteLat') && usp.has('SiteLon')) { let lat = parseFloat(usp.get('SiteLat')); let lon = parseFloat(usp.get('SiteLon')); if (!isNaN(lat) && !isNaN(lon)) { if (true || usp.has('SiteNosave')) { SiteLat = CenterLat = DefaultCenterLat = lat; SiteLon = CenterLon = DefaultCenterLon = lon; SiteOverride = true; } else { loStore['SiteLat'] = lat; loStore['SiteLon'] = lon; } } } if (loStore['SiteLat'] != null && loStore['SiteLon'] != null) { if (usp.has('SiteClear')) { loStore.removeItem('SiteLat'); loStore.removeItem('SiteLon'); } else if (!usp.has('SiteNosave')) { SiteLat = CenterLat = DefaultCenterLat = parseFloat(loStore['SiteLat']); SiteLon = CenterLon = DefaultCenterLon = parseFloat(loStore['SiteLon']); SiteOverride = true; } } if (usp.has('allTracks')) { SelectedAllPlanes = true; buttonActive('#T', SelectedAllPlanes); } if (usp.has('tempTrails')) { tempTrails = true; let tmp = parseInt(usp.get('tempTrails')); if (tmp > 0) tempTrailsTimeout = tmp; } if (usp.has('squareMania')) { squareMania = true; } if (usp.has('darkerColors')) { darkerColors = true; } if (usp.has('mapDim')) { let dim = parseFloat(usp.get('mapDim')); if (!isNaN(dim)) mapDimPercentage = dim; } else if (heatmap) { mapDimPercentage = 0.6; MapDim = true; } if (usp.has('noRegOnly')) noRegOnly = true; if (usp.has('nogpsOnly') || usp.has('badgps')) nogpsOnly = true; if (usp.has('mapContrast')) { let contrast = parseFloat(usp.get('mapContrast')); if (!isNaN(contrast)) mapContrastPercentage = contrast; } if (value = usp.getFloat('labelScale')) { labelScale = value; } if (value = usp.getFloat('largeMode')) { userScale = Math.pow(1.2, value) / 1.2; iconScale = 1; } if (value = usp.getFloat('iconScale')) { iconScale = value; } else if (loStore['iconScale'] != null) { iconScale = loStore['iconScale']; } if (value = usp.getFloat('scale')) { userScale = value; } else if (loStore['userScale'] != null) { userScale = loStore['userScale']; } const slideBase = 0.6; jQuery('#iconScaleSlider').slider({ value: Math.pow(iconScale, 1 / slideBase), step: 0.02, min: 0.1, max: 3, change: function(event, ui) { iconScale = Math.pow(ui.value, slideBase); checkScale(); mapRefresh(); loStore['iconScale'] = iconScale; }, }); jQuery('#userScaleSlider').slider({ value: Math.pow(userScale, 1 / slideBase), step: 0.02, min: 0.5, max: 3, change: function(event, ui) { userScale = Math.pow(ui.value, slideBase); checkScale(); mapRefresh(); loStore['userScale'] = userScale; setGlobalScale(userScale); }, }); setGlobalScale(userScale, "init"); if (usp.has('hideButtons')) hideButtons = true; if (usp.has('baseMap')) { overrideMapType = usp.get('baseMap'); } if (usp.has('offlineMap')) { overrideMapType = 'osm_tiles_offline'; } if (usp.has('overlays')) enableOverlays = usp.get('overlays').split(','); if (value = usp.get('icaoFilter')) { icaoFilter = value.toLowerCase().split(','); } if (value = usp.getFloat('filterMaxRange')) { filterMaxRange = value; } filterMaxRange *= 1852; // convert from nmi to meters if (value = usp.getFloat('mapOrientation')) { mapOrientation = value; } mapOrientation *= (Math.PI/180); // adjust to radians if (usp.has('r') || usp.has('replay')) { let numbers = (usp.get('r') || usp.get('replay') || "").split(/(?:-|:)/); let ts = new Date(); if (numbers.length == 5) { ts.setUTCFullYear(numbers[0]); ts.setUTCMonth(numbers[1] - 1); ts.setUTCDate(numbers[2]); ts.setUTCHours(numbers[3]); ts.setUTCMinutes(numbers[4]); } if (isNaN(ts)) { ts = new Date(); } console.log(ts); replay = replayDefaults(ts); } //Pulling filters from params if (usp.has('filterAltMin')) { const minAlt = usp.getInt('filterAltMin'); if (minAlt !== null) { PlaneFilter.minAltitude = minAlt; PlaneFilter.enabled = true; PlaneFilter.maxAltitude = 1000000; } } if (usp.has('filterAltMax')) { const maxAlt = usp.getInt('filterAltMax'); if (maxAlt !== null) { PlaneFilter.maxAltitude = maxAlt; PlaneFilter.enabled = true; if (PlaneFilter.minAltitude === undefined) { PlaneFilter.minAltitude = -1000000; } } } if (usp.has('filterSources')) { PlaneFilter.sources = usp.get('filterSources').split(','); shareFiltersParam = true; } if (usp.has('filterDbFlag')) { PlaneFilter.flagFilter = usp.get('filterDbFlag').split(','); shareFiltersParam = true; } if (false && iOSVersion() <= 12 && !('PointerEvent' in window)) { jQuery("#generic_error_detail").text("Enable Settings - Safari - Advanced - Experimental features - Pointer Events"); jQuery("#generic_error").css('display','block'); setTimeout(function() { jQuery("#generic_error").css('display','none'); }, 30000); } if (loStore['enableLabels'] == 'true' || usp.has('enableLabels')) { toggleLabels(); } if (usp.has('extendedLabels')) { extendedLabels = parseInt(usp.getFloat('extendedLabels')); toggleExtendedLabels({ noIncrement: true }); } else if (loStore['extendedLabels']) { extendedLabels = parseInt(loStore['extendedLabels']); toggleExtendedLabels({ noIncrement: true }); } if (loStore['trackLabels'] == "true" || usp.has('trackLabels')) { toggleTrackLabels(); } if (loStore['tableInView'] == "true" || usp.has('tableInView')) { toggleTableInView('enable'); } if (loStore['debug'] == "true") debug = true; if (loStore['debugPosFilter'] == "true") debugPosFilter = true; if (loStore['noVanish'] == "true" || usp.has('noVanish')) { noVanish = true; //filterTracks = noVanish; //loStore['noVanish'] = "false"; buttonActive('#P', noVanish); } jQuery('#tabs').tabs({ active: loStore['active_tab'], activate: function (event, ui) { loStore['active_tab'] = jQuery("#tabs").tabs("option", "active"); }, collapsible: true }); // Set page basics document.title = PageName; initializeUnitsSelector(); TAR.planeMan.init(); if (loStore['sidebar_width'] != null) jQuery('#sidebar_container').width(loStore['sidebar_width']); else jQuery('#sidebar_container').width('25%'); if (jQuery('#sidebar_container').width() > jQuery(window).innerWidth() *0.8) jQuery('#sidebar_container').width('30%'); loStore['sidebar_width'] = jQuery('#sidebar_container').width(); jQuery('#sidebar_container').on('resize', function() { loStore['sidebar_width'] = jQuery('#sidebar_container').width(); }); // Set up event handlers for buttons jQuery("#expand_sidebar_button").click(expandSidebar); jQuery("#shrink_sidebar_button").click(showMap); // Set up altitude filter button event handlers and validation options jQuery("#altitude_filter_form").submit(onFilterByAltitude); jQuery("#source_filter_form").submit(updateSourceFilter); jQuery("#flag_filter_form").submit(updateFlagFilter); jQuery("#altitude_filter_reset_button").click(onResetAltitudeFilter); jQuery("#source_filter_reset_button").click(onResetSourceFilter); jQuery("#flag_filter_reset_button").click(onResetFlagFilter); // Initialize other controls jQuery("#search_form").submit(onSearch); jQuery("#search_clear_button").click(onSearchClear); jQuery("#jump_clear_button").click(function() { jQuery("#jump_input").val(""); jQuery("#jump_input").blur(); }); jQuery("#jump_form").submit(onJump); jQuery("#show_trace").click(toggleShowTrace); jQuery("#trace_back_1d").click(function() {shiftTrace(-1)}); jQuery("#trace_jump_1d").click(function() {shiftTrace(1)}); jQuery("#histDatePicker").datepicker({ maxDate: '+1d', dateFormat: "yy-mm-dd", onSelect: function(date){ setTraceDate({string: date}); shiftTrace(); jQuery("#histDatePicker").blur(); }, autoSize: true, onClose: !onMobile ? null : function(dateText, inst){ jQuery("#histDatePicker").attr("disabled", false); }, beforeShow: !onMobile ? null : function(input, inst){ jQuery("#histDatePicker").attr("disabled", true); }, }); jQuery("#replayPlay").click(function(){ if (replay.playing){ //if playing, pause. playReplay(false); } else { //if paused, play. playReplay(true); } }); jQuery("#leg_prev").click(function() {legShift(-1)}); jQuery("#leg_next").click(function() {legShift(1)}); jQuery('#settingsCog').on('click', function() { jQuery('#settings_infoblock').toggle(); }); jQuery('#settings_close').on('click', function() { jQuery('#settings_infoblock').hide(); }); jQuery('#groundvehicle_filter').on('click', function() { filterGroundVehicles(true); refresh(); }); jQuery('#blockedmlat_filter').on('click', function() { filterBlockedMLAT(true); refresh(); }); jQuery('#grouptype_checkbox').on('click', function() { if (jQuery('#grouptype_checkbox').hasClass('settingsCheckboxChecked')) { TAR.planeMan.cols.distance.sort(); } else { TAR.planeMan.cols.data_source.sort(); } }); new Toggle({ key: "lastLeg", display: "Last Leg only", container: "#settingsLeft", init: true, setState: function(state) { lastLeg = state; if (loadFinished && !showTrace) { for (let i in SelPlanes) { SelPlanes[i].processTrace(); } } } }); new Toggle({ key: "labelsGeom", display: "Labels: geom. alt. (WGS84)", container: "#settingsLeft", init: labelsGeom, setState: function(state) { labelsGeom = state; if (loadFinished) { remakeTrails(); refreshSelected(); } } }); new Toggle({ key: "geomUseEGM", display: "Geom. alt.: WGS84 -> EGM conversion (long load)", container: "#settingsLeft", init: geomUseEGM, setState: function(state) { geomUseEGM = state; if (geomUseEGM) { jQuery('#selected_altitude_geom1') jQuery('#selected_altitude_geom1_title').updateText('EGM96 altitude'); jQuery('#selected_altitude_geom2_title').updateText('Geom. EGM96'); let egm = loadEGM(); if (egm) { egm.addEventListener('load', function() { remakeTrails(); refreshSelected(); }); return; } } else { jQuery('#selected_altitude_geom1_title').updateText('WGS84 altitude'); jQuery('#selected_altitude_geom2_title').updateText('Geom. WGS84'); } if (loadFinished) { remakeTrails(); refreshSelected(); } } }); if (usp.has('labelsGeom')) { toggles['labelsGeom'].toggle(true, 'init'); } if (usp.has('geomEGM')) { toggles['geomUseEGM'].toggle(true, 'init'); } new Toggle({ key: "utcTimesLive", display: "Live track labels: UTC", container: "#settingsLeft", init: utcTimesLive, setState: function(state) { utcTimesLive = state; remakeTrails(); refreshSelected(); } }); new Toggle({ key: "utcTimesHistoric", display: "Historic track labels: UTC", container: "#settingsLeft", init: utcTimesHistoric, setState: function(state) { utcTimesHistoric = state; remakeTrails(); refreshSelected(); } }); new Toggle({ key: "windLabelsSlim", display: "Smaller wind labels", container: "#settingsLeft", init: windLabelsSlim, setState: function(state) { windLabelsSlim = state; if (!loadFinished) return; for (let key in g.planesOrdered) { g.planesOrdered[key].updateMarker(); } } }); new Toggle({ key: "showLabelUnits", display: "Label units", container: "#settingsLeft", init: showLabelUnits, setState: function(state) { showLabelUnits = state; if (!loadFinished) return; for (let key in g.planesOrdered) { g.planesOrdered[key].updateMarker(); } remakeTrails(); refreshSelected(); } }); jQuery('#tStop').on('click', function() { traceOpts.replaySpeed = 0; gotoTime(traceOpts.showTime); }); jQuery('#t1x').on('click', function() { replaySpeedChange(1); }); jQuery('#t5x').on('click', function() { replaySpeedChange(5); }); jQuery('#t10x').on('click', function() { replaySpeedChange(10); }); jQuery('#t20x').on('click', function() { replaySpeedChange(20); }); jQuery('#t40x').on('click', function() { replaySpeedChange(40); }); new Toggle({ key: "shareFilters", display: "Include Filters In URLs", container: "#settingsRight", init: false, setState: function(state) { updateAddressBar(); } }); new Toggle({ key: "debugTracks", display: "Debug Tracks", container: "#settingsRight", init: false, setState: function(state) { debugTracks = state; remakeTrails(); } }); new Toggle({ key: "debugAll", display: "Debug show all", container: "#settingsRight", init: false, setState: function(state) { if (state) debugAll = true; else debugAll = false; } }); /* new Toggle({ key: "SiteCircles", display: "Distance Circles", container: "#settingsRight", init: SiteCircles, setState: function(state) { SiteCircles = state; if (loadFinished) initSitePos(); } }); */ new Toggle({ key: "updateLocation", display: "Update GPS location", container: "#settingsRight", init: updateLocation, setState: function(state) { updateLocation = state; runAfterLoad(watchPosition); } }); new Toggle({ key: "autoselect", display: "Auto-select plane", container: "#settingsRight", init: autoselect, setState: function(state) { autoselect = state; setAutoselect(); } }); if (usp.has('autoselect')) { autoselect = true; setAutoselect(); } new Toggle({ key: "ColoredPlanes", display: "Colored Planes", container: "#settingsRight", init: true, setState: function(state) { if (state) monochromeMarkers = null; else monochromeMarkers = "#EEEEEE"; refreshFeatures(); } }); new Toggle({ key: "ColoredTrails", display: "Colored Trails", container: "#settingsRight", init: true, setState: function(state) { if (state) monochromeTracks = null; else monochromeTracks = "#000000"; remakeTrails(); } }); if (globeIndex) { function setGlobeTableLimit() { let mult = 1 + 4 * toggles['moreTableLines1'].state + 16 * (toggles['moreTableLines2'] && toggles['moreTableLines2'].state); globeTableLimit = globeTableLimitBase * mult; if (toggles['allTableLines'] && toggles['allTableLines'].state) globeTableLimit = 1e9; if (onMobile) globeTableLimit /= 2; }; new Toggle({ key: "moreTableLines1", display: "More Table Lines", container: "#sidebar-table", init: false, setState: setGlobeTableLimit, }); new Toggle({ key: "moreTableLines2", display: "Even More Table Lines", container: "#sidebar-table", init: false, setState: setGlobeTableLimit, }); new Toggle({ key: "allTableLines", display: "All Table Lines", container: "#sidebar-table", init: false, setState: setGlobeTableLimit, }); } new Toggle({ key: "sidebar_visible", display: "Sidebar visible", container: null, checkbox: null, button: '#toggle_sidebar_button', init: (onMobile ? false : true), setState: function (state) { if (state) { jQuery("#sidebar_container").show(); jQuery("#expand_sidebar_button").show(); jQuery("#toggle_sidebar_button").removeClass("show_sidebar"); jQuery("#toggle_sidebar_button").addClass("hide_sidebar"); if (!g.sidebar_initiated) { g.sidebar_initiated = true; // Set up map/sidebar splitter jQuery("#sidebar_container").resizable({ handles: { w: '#splitter' }, minWidth: 150, maxWidth: (jQuery(window).innerWidth() *0.8), }); jQuery("#splitter").dblclick(function() { jQuery('#legend').hide(); jQuery('#sidebar_container').width('auto'); updateMapSize(); loStore['sidebar_width'] = jQuery('#sidebar_container').width(); jQuery('#sidebar_container').width(loStore['sidebar_width']); jQuery('#legend').show(); }); } if (!hideButtons) { jQuery('#splitter').show(); } } else { if (loadFinished) { jQuery("#sidebar_container").hide(); jQuery("#expand_sidebar_button").hide(); jQuery("#toggle_sidebar_button").removeClass("hide_sidebar"); jQuery("#toggle_sidebar_button").addClass("show_sidebar"); } } if (loadFinished) { updateMapSize(); } }, }); if (!showPictures) { planespottingAPI = false; planespottersAPI = false; } new Toggle({ key: "planespottingAPI", display: "Pictures planespotting.be", container: "#settingsRight", init: planespottingAPI, setState: function(state) { planespottingAPI = state; if (state) { toggles['planespottersAPI'] && toggles['planespottersAPI'].toggle(false); } setPictureVisibility(); refreshSelected(); } }); new Toggle({ key: "planespottersAPI", display: "Pictures planespotters.net", container: "#settingsRight", init: planespottersAPI, setState: function(state) { planespottersAPI = state; if (state) { toggles['planespottingAPI'] && toggles['planespottingAPI'].toggle(false); } setPictureVisibility(); refreshSelected(); } }); if (useRouteAPI) { new Toggle({ key: "useRouteAPI", display: "Lookup route", container: "#settingsRight", init: useRouteAPI, setState: function(state) { useRouteAPI = state; } }); } new Toggle({ key: "enableInfoblock", display: "Enable Infoblock", container: "#settingsRight", init: true, setState: function(state) { adjustInfoBlock(); } }); new Toggle({ key: "wideInfoblock", display: "Wide Infoblock", container: "#settingsRight", init: wideInfoBlock, setState: function(state) { wideInfoBlock = state; adjustInfoBlock(); } }); if (onMobile) { enableMouseover = false; (typeof hideById != 'undefined') && (hideById) && (hideById('tracking_leaderboard_container')); } new Toggle({ key: "enableMouseover", display: "Enable mouse-over block", container: "#settingsRight", init: enableMouseover, setState: function(state) { enableMouseover = state; if (loadFinished) { checkPointermove(); } } }); jQuery('#selectall_checkbox').on('click', function() { if (jQuery('#selectall_checkbox').hasClass('settingsCheckboxChecked')) { deselectAllPlanes(); } else { selectAllPlanes(); } }) // Force map to redraw if sidebar container is resized - use a timer to debounce jQuery("#sidebar_container").on("resize", function() { clearTimeout(mapResizeTimeout); mapResizeTimeout = setTimeout(updateMapSize, 20); }); filterGroundVehicles(false); filterBlockedMLAT(false); TAR.altitudeChart.init(); if (adsbexchange) { jQuery('#adsbexchange_header').show(); jQuery('#credits').show(); if (!onMobile) { jQuery('#creditsSelected').show(); } jQuery('#selected_infoblock').addClass('adsbx-selected-bg'); if (false && window.self != window.top) { window.top.location.href = "https://www.adsbexchange.com/"; return; } } if (imageConfigLink != "") { let host = window.location.hostname; let configLink = imageConfigLink.replace('HOSTNAME', host); jQuery('#imageConfigLink').attr('href',configLink) jQuery('#imageConfigLink').text(imageConfigText) jQuery('#imageConfigHeader').show(); } } function initLegend(colors) { let html = ''; html += '
ADS-B
'; if (!globeIndex) html += '
UAT / ADS-R
'; if (globeIndex) html += '
ADS-C/R / UAT
'; html += '
MLAT
'; html += '
'; html += '
TIS-B
'; if (!globeIndex) html += '
Mode-S
'; if (globeIndex) html += '
Other
'; document.getElementById('legend').innerHTML = html; } function initSourceFilter(colors) { const createFilter = function (color, text, key) { return '
  • ' + text + '
  • '; }; let html = ''; html += createFilter(colors['adsb'], 'ADS-B', sources[0]); html += createFilter(colors['uat'], 'UAT / ADS-R', sources[1][0]); html += createFilter(colors['mlat'], 'MLAT', sources[2]); html += createFilter(colors['tisb'], 'TIS-B', sources[3]); //if (!globeIndex) html += createFilter(colors['modeS'], 'Mode-S', sources[4]); if (globeIndex) html += createFilter(colors['other'], 'Other', sources[5]); if (globeIndex) html += createFilter(colors['uat'], 'ADS-C', sources[6]); document.getElementById('sourceFilter').innerHTML = html; jQuery("#sourceFilter").selectable({ stop: function () { sourcesFilter = []; jQuery(".ui-selected", this).each(function () { const index = jQuery("#sourceFilter li").index(this); if (Array.isArray(sources[index])) sources[index].forEach(member => { sourcesFilter.push(member); }); else sourcesFilter.push(sources[index]); }); } }); jQuery("#sourceFilter").on("selectablestart", function (event, ui) { event.originalEvent.ctrlKey = true; }); } function initFlagFilter(colors) { const createFilter = function (color, text, key) { return '
  • ' + text + '
  • '; }; let html = ''; html += createFilter(colors['tisb'], 'Military', flagFilterValues[0]); //html += createFilter(colors['mlat'], 'Interesting'); html += createFilter(colors['uat'], 'PIA', flagFilterValues[1]); html += createFilter(colors['adsb'], 'LADD', flagFilterValues[2]); document.getElementById('flagFilter').innerHTML = html; jQuery("#flagFilter").selectable({ stop: function () { flagFilter = []; jQuery(".ui-selected", this).each(function () { const index = jQuery("#flagFilter li").index(this); if (Array.isArray(flagFilterValues[index])) flagFilterValues[index].forEach(member => { flagFilter.push(member); }); else flagFilter.push(flagFilterValues[index]); }); } }); jQuery("#flagFilter").on("selectablestart", function (event, ui) { event.originalEvent.ctrlKey = true; }); } function push_history() { jQuery("#loader_progress").attr('max',nHistoryItems*2); for (let i = 0; i < nHistoryItems; i++) { push_history_item(i); } } function push_history_item(i) { jQuery.when(deferHistory[i]) .done(function(json) { if (HistoryChunks) { if (json && json.files) { for (let i in json.files) { PositionHistoryBuffer.push(json.files[i]); } } else if (json && json.now) { PositionHistoryBuffer.push(json); } } else { PositionHistoryBuffer.push(json); } jQuery("#loader_progress").attr('value',HistoryItemsReturned); HistoryItemsReturned++; if (HistoryItemsReturned == nHistoryItems) { parseHistory(); } }) .fail(function(jqxhr, status, error) { //Doesn't matter if it failed, we'll just be missing a data point jQuery("#loader_progress").attr('value',HistoryItemsReturned); //console.log(error); HistoryItemsReturned++; if (HistoryItemsReturned == nHistoryItems) { parseHistory(); } }); } function parseHistory() { console.timeEnd("Downloaded History"); console.time("Loaded aircraft tracks from History"); for (let i in deferHistory) deferHistory[i] = null; if (PositionHistoryBuffer.length > 0) { // Sort history by timestamp console.log("Sorting history: " + PositionHistoryBuffer.length); PositionHistoryBuffer.sort(function(x,y) { return (y.now - x.now); }); // Process history let data; let h = 0; let pruneInt = Math.floor(PositionHistoryBuffer.length/5); let currentTime = new Date().getTime()/1000; while (data = PositionHistoryBuffer.pop()) { if (pTracks && currentTime - data.now > pTracks * 3600) { continue; } // process new data if (PositionHistoryBuffer.length < 10) { processReceiverUpdate(data, false); } else { processReceiverUpdate(data, true); } if (h==1) { console.log("Applied history " + h + " from: " + (new Date(now * 1000)).toLocaleTimeString()); } // prune aircraft list if (h++ % pruneInt == pruneInt - 1) { console.log("Applied history " + h + " from: " + (new Date(now * 1000)).toLocaleTimeString()); reaper(); } } // Final pass to update all planes to their latest state console.log("Final history cleanup pass"); for (let i in g.planesOrdered) { let plane = g.planesOrdered[i]; if (plane.position && SitePosition && !pTracks) plane.sitedist = ol.sphere.getDistance(SitePosition, plane.position); if (uatNoTISB && plane.uat && plane.type && plane.type.substring(0,4) == "tisb") { plane.last_message_time -= 999; } } refreshFeatures(); TAR.planeMan.refresh(); } PositionHistoryBuffer = null; console.timeEnd("Loaded aircraft tracks from History"); historyLoaded.resolve(); } let replay_was_active = false; let timers = {}; let timersActive = false; function clearIntervalTimers(arg) { timersActive = false; if (loadFinished && arg != 'silent') { jQuery("#timers_paused_detail").text('Timers paused (tab hidden).'); jQuery("#timers_paused").css('display','block'); } console.log("clear timers " + localTime(new Date())); const entries = Object.entries(timers); for (let i in entries) { clearInterval(entries[i][1]); } } function setIntervalTimers() { if (timersActive) { return; } timersActive = true; if (loadFinished) { jQuery("#timers_paused").css('display','none'); } console.log("set timers " + localTime(new Date())); if ((adsbexchange || dynGlobeRate) && !uuid) { timers.globeRateUpdate = setInterval(globeRateUpdate, 180000); } pollPositionInterval(); setAutoselect(); timers.checkMove = setInterval(checkMovement, 50); timers.everySecond = setInterval(everySecond, 850); //timers.reaper = setInterval(reaper, 40000); if (tempTrails) { timers.trailReaper = window.setInterval(trailReaper, 10000); trailReaper(now); } if (enable_pf_data && !pTracks && !globeIndex) { jQuery('#pf_info_container').removeClass('hidden'); timers.pf_data = window.setInterval(fetchPfData, RefreshInterval*10.314); fetchPfData(); } if (receiverJson && receiverJson.outlineJson) { timers.drawOutline = window.setInterval(drawOutlineJson, 15000); drawOutlineJson(); } } let djson; let dstring; let dresult; function startPage() { console.log("Completing init"); if (!globeIndex) { jQuery("#lastLeg_cb").parent().hide(); jQuery('#show_trace').hide(); } if (globeIndex) { toggleTableInView('enable'); if (icaoFilter) { toggleTableInView('disable'); } } else { jQuery('#V').show(); } if (hideButtons) { jQuery('#header_top').hide(); jQuery('#header_side').hide(); jQuery('#tabs').hide(); jQuery('#filterButton').hide(); jQuery('.ol-control').hide(); jQuery('.ol-attribution').show(); } changeZoom("init"); changeCenter("init"); processURLParams(); if (usp.has('reg')) { let req = regIcaoDownload(); req.done(function() { const queries = usp.get('reg').split(','); for (let i in queries) { let icao = db.regCache[queries[i].toUpperCase()]; if (icao) { icao = icao.toLowerCase(); urlIcaos.push(icao); } } processURLParams(); }); } loadFinished = true; // Kick off first refresh. fetchData(); clearIntervalTimers(); setIntervalTimers(); if (tempTrails) selectAllPlanes(); if (!heatmap) jQuery("#loader").hide(); if (replay) { showReplayBar(); loadReplay(replay.ts); } if (heatmap) { drawHeatmap(); } initVisibilityChange(); if (pTracks) setTimeout(TAR.planeMan.refresh, 10000); window.addEventListener("beforeunload", function (event) { //jQuery("#map_canvas").hide(); clearIntervalTimers('silent'); }); if (heatmap || replay || showTrace || pTracks || inhibitFetch) { afterFirstFetch(); } } // // Utils begin // (function (global, jQuery, TAR) { let utils = TAR.utils = TAR.utils || {}; // Make a LineString with 'points'-number points // that is a closed circle on the sphere such that the // great circle distance from 'center' to each point is // 'radius' meters utils.make_geodesic_circle = function (center, radius, points) { const angularDistance = radius / 6378137.0; const lon1 = center[0] * Math.PI / 180.0; const lat1 = center[1] * Math.PI / 180.0; let geom; for (let i = 0; i <= points; ++i) { const bearing = i * 2 * Math.PI / points; let lat2 = Math.asin(Math.sin(lat1) * Math.cos(angularDistance) + Math.cos(lat1) * Math.sin(angularDistance) * Math.cos(bearing)); let lon2 = lon1 + Math.atan2(Math.sin(bearing) * Math.sin(angularDistance) * Math.cos(lat1), Math.cos(angularDistance) - Math.sin(lat1) * Math.sin(lat2)); lat2 = lat2 * 180.0 / Math.PI; lon2 = lon2 * 180.0 / Math.PI; if (!geom) geom = new ol.geom.LineString([[lon2, lat2]]); else geom.appendCoordinate([lon2, lat2]); } return geom; } return TAR; }(window, jQuery, TAR || {})); // // Utils end // // function webglAddLayer() { let success = false; const icao = '~c0ffee'; if (icaoFilter != null) { icaoFilter.push(icao); } processAircraft({hex: icao, lat: CenterLat, lon: CenterLon, type: 'tisb_other', seen: 0, seen_pos: 0, alt_baro: 25000, }); let plane = g.planes['~c0ffee']; let spriteSrc = spritesDataURL ? spritesDataURL : 'images/sprites.png'; //console.log(spriteSrc); try { let glStyle = { symbol: { symbolType: 'image', src: spriteSrc, size: [ 'get', 'size' ], offset: [0, 0], textureCoord: [ 'array', [ 'get', 'cx' ], [ 'get', 'cy' ], [ 'get', 'dx' ], [ 'get', 'dy' ] ], color: [ 'color', [ 'get', 'r' ], [ 'get', 'g' ], [ 'get', 'b' ], 1 ], rotateWithView: false, rotation: [ 'get', 'rotation' ], }, }; if (heatmap) { glStyle = { symbol: { symbolType: "circle", size: heatmap.radius * globalScale * 2.5, offset: [0, 0], opacity: heatmap.alpha || 1, color: [ 'color', [ 'get', 'r' ], [ 'get', 'g' ], [ 'get', 'b' ], 1 ], } } } webglLayer = new ol.layer.WebGLPoints({ name: 'webglLayer', type: 'overlay', title: 'Aircraft pos. webGL', source: webglFeatures, declutter: false, zIndex: 200, style: glStyle, renderBuffer: renderBuffer, }); if (!webglLayer || !webglLayer.getRenderer()) return false; layers.push(webglLayer); webgl = true; plane.visible = true; plane.updateMarker(); OLMap.renderSync(); success = true; } catch (error) { try { layers.remove(webglLayer); } catch (error) { console.error(error); } console.error(error); loStore['webglFailStamp'] = new Date().getTime(); success = false; if (loStore['webgl'] == 'true') loStore.removeItem('webgl'); } delete g.planes[plane.icao]; g.planesOrdered.splice(g.planesOrdered.indexOf(plane), 1); plane.destroy(); if (icaoFilter != null) { icaoFilter.pop(icao); } return success; } function webglInit() { let init = true; // if webGL failed in the last 7 days, don't even try unless people click the toggle. if (loStore['webglFailStamp'] && Number(loStore['webglFailStamp']) + 7 * 24 * 3600 * 1000 > new Date().getTime()) { init = false; if (loStore['webgl'] == undefined) console.log('webGL failed in the past 7 days, not even trying to initialize it'); } new Toggle({ key: "webgl", display: "WebGL", container: "#settingsRight", init: init, setState: function(state) { if (state) { if (webglLayer) { webgl = true; } else { webgl = webglAddLayer(); } if (!webgl) { console.error('Unable to initialize the webGL Layer! Falling back to non-webGL icons, performance will be reduced significantly!'); webglLayer = null; } if (!webgl) return false; // returning false means the toggle will flip back as the activation of the webgl layer was unsuccessful. } else { webgl = false; if (loadFinished) { webglFeatures && webglFeatures.clear(); for (let i in g.planesOrdered) { const plane = g.planesOrdered[i]; delete plane.glMarker; } } } if (loadFinished) { refreshFilter(); checkPointermove(); } }, }); } function ol_map_init() { if (0) { let canvas = iconTest(); spritesDataURL = canvas.toDataURL(); jQuery('#iconTestCanvas').remove(); console.log(spritesDataURL); } OLMap = new ol.Map({ target: 'map_canvas', layers: layers_group, view: new ol.View({ center: ol.proj.fromLonLat([CenterLon, CenterLat]), zoom: zoomLvl, multiWorld: true, }), controls: [new ol.control.Zoom({delta: 1, duration: 0, target: 'map_canvas',}), new ol.control.Attribution({collapsed: true}), new ol.control.ScaleLine({units: DisplayUnits}) ], interactions: new ol.interaction.defaults({altShiftDragRotate:false, pinchRotate:false,}), }); console.time('webglInit'); webglInit(); console.timeEnd('webglInit'); let foundType = false; ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) { if (lyr.get('name') && lyr.get('type') == 'base') { if (MapType_tar1090 == lyr.get('name')) { foundType = true; } } }); if (!foundType) { MapType_tar1090 = "osm_adsbx"; } ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) { if (!lyr.get('name')) return; if (lyr.get('type') == 'base') { if (MapType_tar1090 == lyr.get('name')) { foundType = true; lyr.setVisible(true); } else { lyr.setVisible(false); } lyr.on('change:visible', function(evt) { if (evt.target.getVisible()) { MapType_tar1090 = loStore['MapType_tar1090'] = evt.target.get('name'); mapTypeSettings(); } }); } else if (lyr.get('type') === 'overlay') { if ( loStore['layer_' + lyr.get('name')] == 'true' || enableOverlays.indexOf(lyr.get('name')) >= 0 || (loStore['layer_' + lyr.get('name')] != 'false' && defaultOverlays.indexOf(lyr.get('name')) >= 0) ) { lyr.setVisible(true); } if (loStore['layer_' + lyr.get('name')] == 'false') { lyr.setVisible(false); } lyr.on('change:visible', function(evt) { loStore['layer_' + evt.target.get('name')] = evt.target.getVisible(); }); } }) if (!foundType) { ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) { if (foundType) return; if (lyr.get('type') === 'base') { lyr.setVisible(true); foundType = true; } }); } OLProj = OLMap.getView().getProjection(); OLProjExtent = OLProj.getExtent(); OLMap.getView().setRotation(mapOrientation); // adjust orientation OLMap.addControl(new ol.control.LayerSwitcher({ groupSelectStyle: 'none', activationMode: 'click', // click sucks in the current implementation target: 'map_canvas', })); OLMap.on('movestart', function(event) { webgl && TrackedAircraftPositions > 2000 && webglLayer.setOpacity(0.25); }); OLMap.on('moveend', function(event) { checkMovement(); webgl && webglLayer.setOpacity(1); }); OLMap.on(['click', 'dblclick'], function(evt) { let trailHex = null; let trailTS = null; let planeHex = null; let features = webgl ? webglFeatures : PlaneIconFeatures; let evtCoords = evt.map.getCoordinateFromPixel(evt.pixel); let feature = features.getClosestFeatureToCoordinate(evtCoords); if (feature) { let fPixel = evt.map.getPixelFromCoordinate(feature.getGeometry().getCoordinates()); let a = fPixel[0] - evt.pixel[0]; let b = fPixel[1] - evt.pixel[1]; let c = globalScale * (onMobile ? 30 : 20); if (a**2 + b**2 < c**2) planeHex = feature.hex; } if (!planeHex || showTrace) { let features = evt.map.getFeaturesAtPixel( evt.pixel, { layerFilter: function(layer) { return (layer.get('isTrail') == true); }, hitTolerance: globalScale * (onMobile ? 30 : 20), } ); if (features.length > 0) { let close = 10000000000000; let closest = features[0]; for (let j in features) { let feature = features[j]; let coords; if (feature.isLabel) coords = [feature.getGeometry().getCoordinates()]; else coords = feature.getGeometry().getCoordinates(); for (let k in coords) { let fPixel = evt.map.getPixelFromCoordinate(coords[k]); let a = fPixel[0] - evt.pixel[0]; let b = fPixel[1] - evt.pixel[1]; let distance = a**2 + b**2; if (distance < close) { closest = feature; close = distance; } } } if (showTrace) trailTS = closest.timestamp; else trailHex = closest.hex; } } const dblclick = (evt.type === 'dblclick') && !showTrace; if (showTrace && trailTS) { gotoTime(trailTS); } let hex = planeHex || trailHex; if (hex) { selectPlaneByHex(hex, {noDeselect: dblclick, follow: dblclick}); } if (!hex && !multiSelect && !showTrace) { if (onlySelected) toggleIsolation(); deselect(SelectedPlane); refreshFilter(); } evt.stopPropagation(); }); // show the hover box if (!globeIndex && zoomLvl > 5.5 && enableMouseover) { OLMap.on('pointermove', onPointermove); } } // Initalizes the map and starts up our timers to call various functions function initMap() { if (globeIndex && adsbexchange) { jQuery('#dump1090_total_history_td').hide(); jQuery('#dump1090_message_rate_td').hide(); } // Load stored map settings if present CenterLon = Number(loStore['CenterLon']) || DefaultCenterLon; CenterLat = Number(loStore['CenterLat']) || DefaultCenterLat; zoomLvl = Number(loStore['zoomLvl']) || DefaultZoomLvl; zoomLvlCache = zoomLvl; if (overrideMapType) MapType_tar1090 = overrideMapType; else if (loStore['MapType_tar1090']) { MapType_tar1090 = loStore['MapType_tar1090']; } mapTypeSettings(); // Initialize OpenLayers layers_group = createBaseLayers(); layers = layers_group.getLayers(); //add_kml_overlay('https://developers.google.com/kml/documentation/KML_Samples.kml', 'samples', 0.8); siteCircleLayer = new ol.layer.Vector({ name: 'siteCircles', type: 'overlay', title: 'Range rings', source: siteCircleFeatures, visible: SiteCircles, zIndex: 100, renderOrder: null, renderBuffer: renderBuffer, }); layers.push(siteCircleLayer); siteCircleLayer.on('change:visible', function(evt) { if (evt.target.getVisible()) { runAfterLoad(geoFindMe); } }); locationDotLayer = new ol.layer.Vector({ name: 'locationDot', type: 'overlay', title: (receiverJson && receiverJson.lat != null) ? 'Site position' : 'Your position', source: locationDotFeatures, visible: SiteShow, zIndex: 100, renderOrder: null, renderBuffer: renderBuffer, }); layers.push(locationDotLayer); locationDotLayer.on('change:visible', function(evt) { if (evt.target.getVisible()) { runAfterLoad(geoFindMe); } }); if (receiverJson && receiverJson.outlineJson) { actualOutlineFeatures = new ol.source.Vector(); actualOutlineStyle = new ol.style.Style({ fill: null, stroke: new ol.style.Stroke({ color: actual_range_outline_color, width: actual_range_outline_width, lineDash: actual_range_outline_dash, }), }); actualOutlineLayer = new ol.layer.Vector({ name: 'actualRangeOutline', type: 'overlay', title: 'actual range outline', source: actualOutlineFeatures, zIndex: 101, renderBuffer: renderBuffer, style: actualOutlineStyle, visible: actual_range_show, }); layers.push(actualOutlineLayer); } if (calcOutlineData) { calcOutlineLayer = new ol.layer.Vector({ name: 'calcOutline', type: 'overlay', title: 'terrain-based range outline', source: calcOutlineFeatures, zIndex: 100, renderOrder: null, renderBuffer: renderBuffer, }); layers.push(calcOutlineLayer); drawUpintheair(); } const dummyLayer = new ol.layer.Vector({ name: 'dummy', renderOrder: null, }); trailGroup.push(dummyLayer); trailLayers = new ol.layer.Group({ name: 'ac_trail', title: 'Aircraft trails', type: 'overlay', layers: trailGroup, zIndex: 150, }); layers.push(trailLayers); iconLayer = new ol.layer.Vector({ name: 'iconLayer', type: 'overlay', title: 'Aircraft positions', source: PlaneIconFeatures, declutter: false, zIndex: 200, renderBuffer: renderBuffer, }); layers.push(iconLayer); ol_map_init(); // handle the layer settings pane checkboxes //OLMap.once('postrender', function(e) { //toggleLayer('#nexrad_checkbox', 'nexrad'); //toggleLayer('#sitepos_checkbox', 'site_pos'); //toggleLayer('#actrail_checkbox', 'ac_trail'); //toggleLayer('#acpositions_checkbox', 'webglLayer'); //}); jQuery('#infoblock_close').on('click', function () { if (showTrace) toggleShowTrace(); if (onlySelected) toggleIsolation(); deselect(SelectedPlane); refreshFilter(); }); new Toggle({ key: "darkerColors", display: "Darker Colors", container: "#settingsLeft", init: darkerColors, setState: function(state) { darkerColors = state; if (loadFinished) { refreshFeatures(); remakeTrails(); } } }); tableColorsLight = tableColors; tableColorsDark = JSON.parse(JSON.stringify(tableColors)); let darkVals = Object.values(tableColorsDark); for (let i in ['selected', 'unselected']) { let obj = darkVals[i]; let keys = Object.keys(obj) for (let j in keys) { let key = keys[j]; let hsl = hexToHSL(obj[key]); hsl[1] *= 0.4; hsl[2] *= 0.3; obj[key] = hslToRgb(hsl); } } new Toggle({ key: "darkMode", display: "Dark Mode", container: "#settingsLeft", init: darkModeDefault, setState: function(state) { let root = document.documentElement; jQuery(".layer-switcher .panel").css("background", "var(--BGCOLOR1)"); jQuery(".layer-switcher .panel").css("border", "4px solid var(--BGCOLOR1)"); if (state) { root.style.setProperty("--BGCOLOR1", '#313131'); root.style.setProperty("--BGCOLOR2", '#242424'); root.style.setProperty("--TXTCOLOR1","#BFBFBF"); root.style.setProperty("--TXTCOLOR2","#D8D8D8"); root.style.setProperty("--TXTCOLOR3","#a8a8a8"); //invert the "x" images jQuery(".infoblockCloseBox").css('filter','invert(100%)'); jQuery(".infoblockCloseBox").css(' -webkit-filter','invert(100%)'); jQuery(".settingsCloseBox").css('filter','invert(100%)'); jQuery(".settingsCloseBox").css(' -webkit-filter','invert(100%)'); tableColors = tableColorsDark; } else { root.style.setProperty("--BGCOLOR1", '#F8F8F8'); root.style.setProperty("--BGCOLOR2", '#CCCCCC'); root.style.setProperty("--TXTCOLOR1","#003f4b"); root.style.setProperty("--TXTCOLOR2","#050505"); root.style.setProperty("--TXTCOLOR3","#003f4b"); jQuery(".infoblockCloseBox").css('filter','invert(0%)'); jQuery(".infoblockCloseBox").css(' -webkit-filter','invert(0%)'); jQuery(".settingsCloseBox").css('filter','invert(0%)'); jQuery(".settingsCloseBox").css(' -webkit-filter','invert(0%)'); tableColors = tableColorsLight; } if (loadFinished) { TAR.planeMan.redraw(); refreshFilter(); initLegend(tableColors.unselected); initSourceFilter(tableColors.unselected); initFlagFilter(tableColors.unselected); } } }); initLegend(tableColors.unselected); initFilters(); new Toggle({ key: "MapDim", display: "Dim Map", container: "#settingsLeft", init: MapDim, setState: function(state) { if (!state) { ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) { if (lyr.get('type') != 'base') return; ol.Observable.unByKey(lyr.dimKey); }); } else { ol.control.LayerSwitcher.forEachRecursive(layers_group, function(lyr) { if (lyr.get('type') != 'base') return; lyr.dimKey = lyr.on('postrender', dim); }); } if (loadFinished) { OLMap.render(); } buttonActive('#B', state); } }); window.addEventListener('keydown', function(e) { active(); if (e.defaultPrevented ) { return; // Do nothing if the event was already processed } if (e.target.type == "text") { return; } if (e.srcElement.nodeName == 'INPUT') { return; } if( e.ctrlKey || e.altKey || e.metaKey) { return; } let oldCenter, extent, newCenter; switch (e.key) { case "c": case "Esc": case "Escape": deselectAllPlanes(); break; // zoom and movement case "q": case "-": case "Subtract": zoomOut(); break; case "e": case "+": case "Add": zoomIn(); break; case "ArrowUp": case "w": oldCenter = OLMap.getView().getCenter(); extent = OLMap.getView().calculateExtent(OLMap.getSize()); newCenter = [oldCenter[0], (oldCenter[1] + extent[3])/2]; OLMap.getView().setCenter(newCenter); toggleFollow(false); break; case "ArrowDown": case "s": oldCenter = OLMap.getView().getCenter(); extent = OLMap.getView().calculateExtent(OLMap.getSize()); newCenter = [oldCenter[0], (oldCenter[1] + extent[1])/2]; OLMap.getView().setCenter(newCenter); toggleFollow(false); break; case "ArrowLeft": case "a": oldCenter = OLMap.getView().getCenter(); extent = OLMap.getView().calculateExtent(OLMap.getSize()); newCenter = [(oldCenter[0] + extent[0])/2, oldCenter[1]]; OLMap.getView().setCenter(newCenter); toggleFollow(false); break; case "ArrowRight": case "d": oldCenter = OLMap.getView().getCenter(); extent = OLMap.getView().calculateExtent(OLMap.getSize()); newCenter = [(oldCenter[0] + extent[2])/2, oldCenter[1]]; OLMap.getView().setCenter(newCenter); toggleFollow(false); break; // misc case "b": toggles['MapDim'].toggle(); break; case "m": toggleMultiSelect(); break; case "v": toggleTableInView(); break; case "r": if (heatmap) drawHeatmap(); else followRandomPlane(); break; case "R": fetchData(); break; case "t": selectAllPlanes(); break; case "G": nogpsOnly = !nogpsOnly; refreshFilter(); break; case "h": resetMap(); break; case "H": if (!hideButtons) { jQuery('#header_top').hide(); jQuery('#header_side').hide(); jQuery('#splitter').hide(); jQuery('#tabs').hide(); jQuery('#filterButton').hide(); jQuery('.ol-control').hide(); jQuery('.ol-attribution').show(); } else { jQuery('#header_top').show(); jQuery('#header_side').show(); jQuery('#splitter').show(); jQuery('#tabs').show(); jQuery('#filterButton').show(); jQuery('.ol-control').show(); jQuery('#expand_sidebar_control').hide(); toggles['sidebar_visible'].restore(); TAR.altitudeChart.render(); } hideButtons = !hideButtons; break; case "f": toggleFollow(); break; // filters case "T": filterTISB = !filterTISB; refreshFilter(); break; case "u": toggleMilitary(); break; case "i": toggleIsolation(); break; // persistence mode case "p": togglePersistence(); break; // Labels case "l": toggleLabels(); break; case "o": toggleExtendedLabels(); break; case "k": toggleTrackLabels(); break; // debug stuff case "L": toggles['lastLeg'].toggle(); break; case "D": debug = !debug; loStore['debug'] = debug; console.log('debug = ' + debug); break; case "P": debugPosFilter = !debugPosFilter; loStore['debugPosFilter'] = debugPosFilter; console.log('debugPosFilter = ' + debugPosFilter); break; case "?": if (!SelectedPlane) { console.log("No plane selected"); break; } console.log(SelectedPlane.icao + ": " + SelectedPlane.baseMarkerKey + " " + SelectedPlane.shape); console.log(SelectedPlane); console.log(SelectedPlane.milRange()); break; case "j": selectPlaneByHex(jumpTo, {follow: true}); break; case "J": debugJump = !debugJump; loStore['debugJump'] = debugJump; console.log('debugJump = ' + debugJump); break; case "N": noMLAT = !noMLAT; loStore['noMLAT'] = noMLAT; console.log('noMLAT = ' + noMLAT); break; } }, true); if (!usp.has('icao') && !usp.has("lat") && !usp.has("lon") && !usp.has('airport') ) { runAfterLoad(geoFindMe); } else { runAfterLoad(initSitePos); } } // This looks for planes to reap out of the master g.planes variable let lastReap = 0; let reapInProgress = false; function reaper(all) { //console.log("Reaping started.."); if (reapInProgress) { return; } lastReap = now; if (noVanish && !all) { return; } reapInProgress = true; if (!all) { releaseMem(); } // Look for planes where we have seen no messages for >300 seconds let plane; let length = g.planesOrdered.length; let temp = [] for (let i in g.planesOrdered) { plane = g.planesOrdered[i]; if (plane == null) continue; plane.seen = now - plane.last_message_time; if ( all || ((!plane.selected) && plane.seen > 300 && (plane.dataSource != 'adsc' || plane.seen > jaeroTimeout)) ) { // Reap it. //console.log("Removed " + plane.icao); delete g.planes[plane.icao]; plane.destroy(); continue; } // Keep it. temp.push(plane); if (globeIndex) { if (plane.clearTraceAfter) { //console.log(now - plane.clearTraceAfter); if (now > plane.clearTraceAfter) { plane.clearTrace(); //console.log("clearTrace: " + plane.icao); } } else if (!plane.linesDrawn) { plane.clearTraceAfter = now + 300; } } } g.planesOrdered = temp; reapInProgress = false; const removed = length - g.planesOrdered.length; if (removed > 0) { //console.log(`reaper removed ${removed} planes.`); } return removed; } // Page Title update function function refreshPageTitle() { if (pTracks) return; if (!PlaneCountInTitle && !MessageRateInTitle) { return; } let subtitle = ""; if (PlaneCountInTitle) { if (globeIndex) { subtitle += 'tracking ' + globeTrackedAircraft + ' aircraft'; } else { subtitle += TrackedAircraftPositions + '/' + TrackedAircraft; } } if (MessageRateInTitle && MessageRate != null) { if (subtitle) subtitle += ' | '; subtitle += MessageRate.toFixed(1) + '/s'; } document.title = PageName + ' - ' + subtitle; } function displaySil() { jQuery('#copyrightInfo').html(""); if (!showSil) { setPhotoHtml(""); return; } let selected = SelectedPlane; let new_html=""; let type = selected.icaoType ? selected.icaoType : 'ZZZZ'; let hex = selected.icao.toUpperCase(); new_html = ""; setPhotoHtml(new_html); selected.icao.toUpperCase(); } function displayPhoto() { if (!SelectedPlane) return; if (!SelectedPlane.psAPIresponse) { displaySil(); return; } let photos = SelectedPlane.psAPIresponse["photos"] || SelectedPlane.psAPIresponse["images"]; if (!photos || photos.length == 0) { displaySil(); adjustInfoBlock(); return; } let new_html=""; let photoToPull = photos[0]["thumbnail"]["src"] || photos[0]["thumbnail"]; let linkToPicture = photos[0]["link"]; //console.log(linkToPicture); new_html = ''; let copyright = photos[0]["photographer"] || photos[0]["user"]; jQuery('#copyrightInfo').html("Image © " + copyright +""); setPhotoHtml(new_html); adjustInfoBlock(); } function refreshPhoto(selected) { if (!showPictures || selected.icao[0] == '~' || (!planespottingAPI && !planespottersAPI)) { displaySil(); return; } let urlTail; let param; if (!selected.dbinfoLoaded) { displaySil(); return; } else if (false && selected.registration != null && selected.registration.match(/^[0-9]{0,2}\+?[0-9]{0,2}$/)) { urlTail = '/hex/' + selected.icao.toUpperCase(); } else if (selected.registration != null) { urlTail = '/hex/' + selected.icao.toUpperCase() + '?reg=' + selected.registration; const type = selected.icaoType; // && type != 'E170' && !type.startsWith('E75') if (type) { urlTail += '&icaoType=' + type; } param = 'DB'; } else { urlTail = 'hex/' + selected.icao.toUpperCase(); param = 'hex'; } const ts = new Date().getTime(); if (param == selected.psAPIparam) { if (selected.psAPIresponse) { displayPhoto(); return; } if (selected.psAPIresponseTS && selected.psAPIresponseTS - ts < 10000) { return; } } selected.psAPIparam = param; setPhotoHtml("

    Loading image...

    "); jQuery('#copyrightInfo').html(""); //console.log(ts/1000 + 'sending psAPI request'); selected.psAPIresponseTS = ts; if (planespottersAPI) { let req = jQuery.ajax({ url: planespottersAPIurl + urlTail, dataType: 'json', plane: selected, }); req.done(function(data) { this.plane.psAPIresponse = data; if (SelectedPlane == this.plane) { displayPhoto(); } }); } else if (planespottingAPI) { let req = jQuery.ajax({ url: 'https://www.planespotting.be/api/objects/imagesRegistration.php?registration=' + selected.registration, dataType: 'json', plane: selected, }); req.done(function(data) { this.plane.psAPIresponse = data; if (SelectedPlane == this.plane) { displayPhoto(); } }); req.fail(function() { this.plane.psAPIresponse = {'photos': []}; if (SelectedPlane == this.plane) { displayPhoto(); } }); } } let selCall = null; let selIcao = null; let selReg = null; let somethingSelected = false; // Refresh the detail window about the plane function refreshSelected() { const selected = SelectedPlane; if (!selected) { if (somethingSelected) { adjustInfoBlock(); buttonActive('#F', FollowSelected); } somethingSelected = false; return; } somethingSelected = true; buttonActive('#F', FollowSelected); selected.updateVisible(); selected.checkForDB(); refreshPhoto(selected); jQuery('#selected_callsign').updateText(selected.name); if (showTrace) { if (selected.position_time) { const date = new Date(selected.position_time * 1000); let timestamp = utcTimesHistoric ? (zuluTime(date) + NBSP + 'Z') : (lDateString(date) + ' ' + localTime(date) + NBSP + TIMEZONE); jQuery('#trace_time').updateText('Time:\n' + timestamp); } else { jQuery('#trace_time').updateText('Time:\n'); } } if (flightawareLinks) { jQuery('#selected_flightaware_link').html(getFlightAwareModeSLink(selected.icao, selected.flight, "Visit Flight Page")); } if (selected.isNonIcao() && selected.source != 'mlat') { jQuery('#anon_mlat_info').addClass('hidden'); jQuery('#reg_info').addClass('hidden'); jQuery('#tisb_info').removeClass('hidden'); } else if (selected.isNonIcao() && selected.source == 'mlat') { jQuery('#reg_info').addClass('hidden'); jQuery('#tisb_info').addClass('hidden'); jQuery('#anon_mlat_info').removeClass('hidden'); } else { jQuery('#tisb_info').addClass('hidden'); jQuery('#anon_mlat_info').addClass('hidden'); jQuery('#reg_info').removeClass('hidden'); } let checkReg = selected.registration + ' ' + selected.dbinfoLoaded; if (checkReg != selReg) { selReg = checkReg; if (selected.registration) { if (flightawareLinks) { jQuery('#selected_registration').html(getFlightAwareIdentLink(selected.registration, selected.registration)); } else if (registrationLinks && registrationLink(selected)) { jQuery('#selected_registration').html(`${selected.registration}`); } else { jQuery('#selected_registration').updateText(selected.registration); } } else { jQuery('#selected_registration').updateText("n/a"); } } let dbFlags = ""; if (selected.ladd) dbFlags += ' LADD / '; if (selected.pia) dbFlags += 'PIA / '; if (selected.military) dbFlags += 'military / '; if (dbFlags.length == 0) { jQuery('#selected_dbFlags').updateText("none"); } else { jQuery('#selected_dbFlags').html(dbFlags.slice(0, -3)); } if (selected.icaoType) { jQuery('#selected_icaotype').updateText(selected.icaoType); } else { jQuery('#selected_icaotype').updateText("n/a"); } if (selected.typeDescription) jQuery('#selected_typedesc').updateText(selected.typeDescription); else jQuery('#selected_typedesc').updateText("n/a"); let typeLine = ""; if (selected.year) typeLine += selected.year + " " if (selected.typeLong) typeLine += selected.typeLong; if (!typeLine) typeLine = "n/a" jQuery('#selected_typelong').updateText(typeLine); if (selected.ownOp) jQuery('#selected_ownop').updateText(selected.ownOp); else jQuery('#selected_ownop').updateText(""); if (selected.rId && show_rId) { jQuery('#receiver_id').updateText(selected.rId); jQuery('#receiver_id_div').removeClass('hidden'); } else { jQuery('#receiver_id_div').addClass('hidden'); } jQuery("#selected_altitude1").updateText(format_altitude_long(selected.altitude, selected.vert_rate, DisplayUnits)); jQuery("#selected_altitude2").updateText(format_altitude_long(selected.altitude, selected.vert_rate, DisplayUnits)); jQuery('#selected_onground').updateText(format_onground(selected.altitude)); if (selected.squawk == null || selected.squawk == '0000') { jQuery('#selected_squawk1').updateText('n/a'); jQuery('#selected_squawk2').updateText('n/a'); } else { jQuery('#selected_squawk1').updateText(selected.squawk); jQuery('#selected_squawk2').updateText(selected.squawk); } if (useRouteAPI) { jQuery('#routeRow').show(); if (selected.routeString) { jQuery('#selected_route').updateText(selected.routeString); } else { jQuery('#selected_route').updateText('n/a'); } } else { jQuery('#routeRow').hide(); } let magResult = null; if (geoMag && selected.position != null) { let lon = selected.position[0]; let lat = selected.position[1]; let alt = selected.altitude == "ground" ? 0 : selected.altitude; magResult = geoMag(lat, lon, alt); jQuery('#selected_mag_declination').updateText(format_track_brief(magResult.dec)); } else { jQuery('#selected_mag_declination').updateText('n/a'); } let heading = null; if (selected.true_heading != null && selected.track != null) { heading = selected.true_heading; } else if (magResult && selected.mag_heading != null && selected.track != null) { heading = selected.mag_heading + magResult.dec; } if (heading != null && heading < 0) heading += 360; if (heading != null && heading > 360) heading -= 360; jQuery('#selected_mag_heading').updateText(format_track_brief(selected.mag_heading)); if (selected.wd != null && selected.ws != null) { jQuery('#selected_wd').updateText(format_track_brief(selected.wd, true)); jQuery('#selected_ws').updateText(format_speed_long(selected.ws, DisplayUnits)); } else if (!globeIndex && magResult && selected.gs != null && selected.tas != null && selected.track != null && selected.mag_heading != null) { const trk = (Math.PI / 180) * selected.track; const hdg = (Math.PI / 180) * heading; const tas = selected.tas; const gs = selected.gs; const ws = Math.round(Math.sqrt(Math.pow(tas - gs, 2) + 4 * tas * gs * Math.pow(Math.sin((hdg - trk) / 2), 2))); let wd = trk + Math.atan2(tas * Math.sin(hdg - trk), tas * Math.cos(hdg - trk) - gs); if (wd < 0) { wd = wd + 2 * Math.PI; } if (wd > 2 * Math.PI) { wd = wd - 2 * Math.PI; } wd = Math.round((180 / Math.PI) * wd); jQuery('#selected_wd').updateText(format_track_brief(wd, true)); jQuery('#selected_ws').updateText(format_speed_long(ws, DisplayUnits)); } else { jQuery('#selected_wd').updateText('n/a'); jQuery('#selected_ws').updateText('n/a'); } if (!globeIndex && selected.true_heading == null && heading != null) jQuery('#selected_true_heading').updateText(format_track_brief(heading)); else jQuery('#selected_true_heading').updateText(format_track_brief(selected.true_heading)); let oat = null; let tat = null; if (selected.tat != null && selected.oat != null) { oat = selected.oat; tat = selected.tat; } else if (!globeIndex && selected.mach != null && selected.tas != null && selected.mach > 0.395) { oat = Math.pow((selected.tas / 661.47 / selected.mach), 2) * 288.15 - 273.15; tat = -273.15 + (oat + 273.15) * (1 + 0.2 * selected.mach * selected.mach); } if (oat != null) jQuery('#selected_temp').updateText(Math.round(tat) + ' / ' + Math.round(oat) + ' °C'); else jQuery('#selected_temp').updateText('n/a'); jQuery('#selected_speed1').updateText(format_speed_long(selected.gs, DisplayUnits)); jQuery('#selected_speed2').updateText(format_speed_long(selected.gs, DisplayUnits)); jQuery('#selected_ias').updateText(format_speed_long(selected.ias, DisplayUnits)); jQuery('#selected_tas').updateText(format_speed_long(selected.tas, DisplayUnits)); jQuery('#selected_vert_rate').updateText(format_vert_rate_long(selected.vert_rate, DisplayUnits)); jQuery('#selected_baro_rate').updateText(format_vert_rate_long(selected.baro_rate, DisplayUnits)); jQuery('#selected_geom_rate').updateText(format_vert_rate_long(selected.geom_rate, DisplayUnits)); setSelectedIcao(); jQuery('#selected_pf_info').updateText((selected.pfRoute ? selected.pfRoute : "") ); //+" "+ (selected.pfFlightno ? selected.pfFlightno : "") jQuery('#airframes_post_icao').attr('value',selected.icao); jQuery('#selected_track1').updateText(format_track_brief(selected.track)); jQuery('#selected_track2').updateText(format_track_brief(selected.track)); if (selected.seen != null && selected.seen < 1000000) { jQuery('#selected_seen').updateText(format_duration(selected.seen)); } else { jQuery('#selected_seen').updateText('n/a'); } if (selected.position_time != null) { jQuery('#selected_pos_epoch').updateText(Math.round(selected.position_time)); } else { jQuery('#selected_pos_epoch').updateText('n/a'); } if (selected.seen_pos != null && selected.seen_pos < 1000000) { jQuery('#selected_seen_pos').updateText(format_duration(selected.seen_pos)); } else { jQuery('#selected_seen_pos').updateText('n/a'); } jQuery('#selected_country').updateText(selected.country.replace("special use", "special")); if (ShowFlags && selected.flag_image !== null) { jQuery('#selected_flag').removeClass('hidden'); jQuery('#selected_flag img').attr('src', FlagPath + selected.flag_image); jQuery('#selected_flag img').attr('title', selected.country); } else { jQuery('#selected_flag').addClass('hidden'); } if (selected.position == null) { jQuery('#selected_position').updateText('n/a'); } else { if (selected.seen_pos > -1) { jQuery('#selected_position').updateText(format_latlng(selected.position)); } else { jQuery('#selected_position').updateText(format_latlng(selected.position)); } } let sitedist; if (selected.position && SitePosition) { sitedist = ol.sphere.getDistance(SitePosition, selected.position); } jQuery('#selected_source').updateText(format_data_source(selected.dataSource)); jQuery('#selected_category').updateText(selected.category ? selected.category : "n/a"); jQuery('#selected_category_label').updateText(get_category_label(selected.category)); jQuery('#selected_sitedist1').updateText(format_distance_long(sitedist, DisplayUnits)); jQuery('#selected_sitedist2').updateText(format_distance_long(sitedist, DisplayUnits)); jQuery('#selected_rssi1').updateText(selected.rssi != null ? selected.rssi.toFixed(1) : "n/a"); if (globeIndex && binCraft && !showTrace) { jQuery('#selected_message_count').prev().updateText('Receivers:'); jQuery('#selected_message_count').prop('title', 'Number of receivers receiving this aircraft'); if (selected.receiverCount >= 5 && selected.dataSource != 'mlat') { jQuery('#selected_message_count').updateText('> ' + selected.receiverCount); } else { jQuery('#selected_message_count').updateText(selected.receiverCount); } } else { jQuery('#selected_message_count').prev().updateText('Messages:'); jQuery('#selected_message_count').prop('title', 'The total number of messages received from this aircraft'); jQuery('#selected_message_count').updateText(selected.messages); } jQuery('#selected_message_rate').updateText((selected.messageRate != null) ? (selected.messageRate.toFixed(1)) : "n/a"); jQuery('#selected_photo_link').html(getPhotoLink(selected)); jQuery('#selected_altitude_geom1').updateText(format_altitude_long(adjust_geom_alt(selected.alt_geom, selected.position), selected.geom_rate, DisplayUnits)); jQuery('#selected_altitude_geom2').updateText(format_altitude_long(adjust_geom_alt(selected.alt_geom, selected.position), selected.geom_rate, DisplayUnits)); jQuery('#selected_ias').updateText(format_speed_long(selected.ias, DisplayUnits)); jQuery('#selected_tas').updateText(format_speed_long(selected.tas, DisplayUnits)); if (selected.mach == null) { jQuery('#selected_mach').updateText('n/a'); } else { jQuery('#selected_mach').updateText(selected.mach.toFixed(3)); } if (selected.roll == null) { jQuery('#selected_roll').updateText('n/a'); } else { jQuery('#selected_roll').updateText(selected.roll.toFixed(1)); } if (selected.track_rate == null) { jQuery('#selected_trackrate').updateText('n/a'); } else { jQuery('#selected_trackrate').updateText(selected.track_rate.toFixed(2)); } jQuery('#selected_geom_rate').updateText(format_vert_rate_long(selected.geom_rate, DisplayUnits)); if (selected.nav_qnh == null) { jQuery('#selected_nav_qnh').updateText("n/a"); } else { jQuery('#selected_nav_qnh').updateText(selected.nav_qnh.toFixed(1) + " hPa"); } jQuery('#selected_nav_altitude').updateText(format_altitude_long(selected.nav_altitude, 0, DisplayUnits)); jQuery('#selected_nav_heading').updateText(format_track_brief(selected.nav_heading)); if (selected.nav_modes == null) { jQuery('#selected_nav_modes').updateText("n/a"); } else { jQuery('#selected_nav_modes').updateText(selected.nav_modes.join()); } if (selected.nic_baro == null) { jQuery('#selected_nic_baro').updateText("n/a"); } else { if (selected.nic_baro == 1) { jQuery('#selected_nic_baro').updateText("cross-checked"); } else { jQuery('#selected_nic_baro').updateText("not cross-checked"); } } jQuery('#selected_nac_p').updateText(format_nac_p(selected.nac_p)); jQuery('#selected_nac_v').updateText(format_nac_v(selected.nac_v)); if (selected.rc == null) { jQuery('#selected_rc').updateText("n/a"); } else if (selected.rc == 0) { jQuery('#selected_rc').updateText("unknown"); } else { jQuery('#selected_rc').updateText(format_distance_short(selected.rc, DisplayUnits)); } if (selected.sil == null || selected.sil_type == null) { jQuery('#selected_sil').updateText("n/a"); } else { let sampleRate = ""; let silDesc = ""; if (selected.sil_type == "perhour") { sampleRate = " per flight hour"; } else if (selected.sil_type == "persample") { sampleRate = " per sample"; } switch (selected.sil) { case 0: silDesc = "> 1e-3"; break; case 1: silDesc = "≤ 1e-3"; break; case 2: silDesc = "≤ 1e-5"; break; case 3: silDesc = "≤ 1e-7"; break; default: silDesc = "n/a"; sampleRate = ""; break; } jQuery('#selected_sil').html(silDesc + sampleRate); } if (selected.version == null) { jQuery('#selected_version').updateText('none'); } else if (selected.version == 0) { jQuery('#selected_version').updateText('v0 (DO-260)'); } else if (selected.version == 1) { jQuery('#selected_version').updateText('v1 (DO-260A)'); } else if (selected.version == 2) { jQuery('#selected_version').updateText('v2 (DO-260B)'); } else { jQuery('#selected_version').updateText('v' + selected.version); } adjustInfoBlock(); } let somethingHighlighted = false; function refreshHighlighted() { // this is following nearly identical logic, etc, as the refreshSelected function, but doing less junk for the highlighted pane let highlighted = HighlightedPlane; if (!highlighted) { if (somethingHighlighted) jQuery('#highlighted_infoblock').hide(); somethingHighlighted = false; return; } somethingHighlighted = true; highlighted.checkVisible(); jQuery('#highlighted_infoblock').show(); let infoBox = jQuery('#highlighted_infoblock'); let marker = highlighted.marker || highlighted.glMarker; let geom; let markerCoordinates; if (!marker || !(geom = marker.getGeometry()) || !(markerCoordinates = geom.getCoordinates()) ) { jQuery('#highlighted_infoblock').hide(); return; } let markerPosition = OLMap.getPixelFromCoordinate(markerCoordinates); if (!markerPosition) return; let mapSize = OLMap.getSize(); let infoBoxLeft = markerPosition[0]; let infoBoxTop = markerPosition[1]; if ((infoBoxLeft + 20 + infoBox.width()) < mapSize[0]) infoBoxLeft += 20; else if ((infoBoxLeft - 20 - infoBox.width()) > 0) infoBoxLeft -= (20 + infoBox.width()); else infoBoxLeft = 0; if ((infoBoxTop + 20 + infoBox.height()) < mapSize[1]) infoBoxTop += 20; else if (infoBoxTop - (20 + infoBox.height()) > 0) infoBoxTop -= (20 + infoBox.height()); else infoBoxTop = 0; infoBox.css("left", infoBoxLeft); infoBox.css("top", infoBoxTop); jQuery('#highlighted_callsign').text(highlighted.name); if (highlighted.icaoType !== null) { jQuery('#highlighted_icaotype').text(highlighted.icaoType); } else { jQuery('#highlighted_icaotype').text("n/a"); } jQuery('#highlighted_source').text(format_data_source(highlighted.getDataSource())); if (highlighted.registration !== null) { jQuery('#highlighted_registration').text(highlighted.registration); } else { jQuery('#highlighted_registration').text("n/a"); } jQuery('#highlighted_speed').text(format_speed_long(highlighted.gs, DisplayUnits)); jQuery("#highlighted_altitude").text(format_altitude_long(highlighted.altitude, highlighted.vert_rate, DisplayUnits)); jQuery('#highlighted_pf_route').text((highlighted.pfRoute ? highlighted.pfRoute : highlighted.icao.toUpperCase())); jQuery('#highlighted_rssi').text(highlighted.rssi != null ? highlighted.rssi.toFixed(1) + ' dBFS' : "n/a"); } function removeHighlight() { HighlightedPlane = null; refreshHighlighted(); } function mstime() { return new Date().getTime(); } // recreating all OpenLayers Features for the planes every now and then releases some retained memory // Haven't been able to create a reproducer ... regardless let's stick with this workaround // in other words: probably not an issue with OpenLayers. let nextCacheClear = mstime() + 300 * 1000; function releaseMem() { if (mstime() > nextCacheClear) { nextCacheClear = mstime() + 300 * 1000; lineStyleCache = {}; iconCache = {}; } //console.trace(); //console.log('releaseMem()'); for (let i in g.planesOrdered) { let plane = g.planesOrdered[i]; plane.clearMarker(); plane.destroyTR(); } refreshFeatures(); TAR.planeMan.redraw(); refresh(); } function refreshFeatures() { if (!loadFinished) { return; } updateVisible(); for (let i in g.planesOrdered) { g.planesOrdered[i].updateFeatures(true); } } // // g.planes table begin // (function (global, jQuery, TAR) { let planeMan = TAR.planeMan = TAR.planeMan || {}; function compareAlpha(xa,ya) { if (xa === ya) return 0; if (xa < ya) return -1; return 1; } function compareBeta(xa, ya) { if (xa === ya) return 0; if (sortAscending && xa < ya) return -1; if (!sortAscending && (xa.replace(/ /g, "").split("").reverse().join("") > ya.replace(/ /g, "").split("").reverse().join(""))) return -1; return 1; } function compareNumeric(xf,yf) { if (Math.abs(xf - yf) < 1e-9) return 0; return xf - yf; } const cols = planeMan.cols = {}; cols.icao = { text: 'Hex ID', sort: function () { sortBy('icao', compareAlpha, function(x) { return x.icao; }); }, value: function(plane) { return plane.icao; }, td: '', }; cols.flag = { text: 'Flag', header: function() { return ""; }, sort: function () { sortBy('country', compareAlpha, function(x) { return x.country; }); }, value: function(plane) { return (plane.flag_image ? ('') : ''); }, hStyle: 'style="width: 20px; padding: 3px;"', html: true, }; cols.flight = { sort: function () { sortBy('flight', compareAlpha, function(x) { return x.flight }); }, value: function(plane) { if (flightawareLinks) return getFlightAwareModeSLink(plane.icao, plane.flight, plane.name); return (plane.flight || ''); }, html: flightawareLinks, text: 'Callsign' }; if (useRouteAPI) { cols.route = { sort: function () { sortBy('route', compareAlpha, function(x) { return x.routeString }); }, value: function(plane) { return ((useRouteAPI && plane.routeString) || ''); }, text: 'Route' }; } cols.registration = { sort: function () { sortBy('registration', compareAlpha, function(x) { return x.registration; }); }, value: function(plane) { return (flightawareLinks ? getFlightAwareIdentLink(plane.registration, plane.registration) : (plane.registration ? plane.registration : "")); }, html: flightawareLinks, text: 'Registration' }; cols.aircraft_type = { sort: function () { sortBy('type', compareAlpha, function(x) { return x.icaoType; }); }, value: function(plane) { return (plane.icaoType != null ? plane.icaoType : ""); }, text: 'Type' }; cols.squawk = { text: 'Squawk', sort: function () { sortBy('squawk', compareAlpha, function(x) { return x.squawk; }); }, value: function(plane) { return (plane.squawk != null ? plane.squawk : ""); }, align: 'right' }; cols.altitude = { text: 'Altitude', sort: function () { sortBy('altitude',compareNumeric, function(x) { return (x.altitude == "ground" ? -100000 : x.altitude); }); }, value: function(plane) { return format_altitude_brief(plane.altitude, plane.vert_rate, DisplayUnits); }, align: 'right', header: function () { return 'Alt.' + NBSP + '(' + get_unit_label("altitude", DisplayUnits) + ')';}, }; cols.speed = { text: pTracks ? 'Max. Speed' : 'Speed', sort: function () { sortBy('speed', compareNumeric, function(x) { return x.speed; }); }, value: function(plane) { return format_speed_brief(plane.speed, DisplayUnits); }, align: 'right', header: function () { return (pTracks ? 'Max. ' : '') + 'Spd.' + NBSP + '(' + get_unit_label("speed", DisplayUnits) + ')';}, }; cols.vert_rate = { text: 'Vertical Rate', sort: function () { sortBy('vert_rate', compareNumeric, function(x) { return x.vert_rate; }); }, value: function(plane) { return format_vert_rate_brief(plane.vert_rate, DisplayUnits); }, align: 'right', header: function () { return 'V. Rate(' + get_unit_label("verticalRate", DisplayUnits) + ')';}, }; cols.distance = { text: pTracks ? 'Max. Distance' : 'Distance', sort: function () { sortBy('sitedist',compareNumeric, function(x) { return x.sitedist; }); }, value: function(plane) { return format_distance_brief(plane.sitedist, DisplayUnits); }, align: 'right', header: function () { return (pTracks ? 'Max. ' : '') + 'Dist.' + NBSP + '(' + get_unit_label("distance", DisplayUnits) + ')';}, }; cols.track = { text: 'Track', sort: function () { sortBy('track', compareNumeric, function(x) { return x.track; }); }, value: function(plane) { return format_track_brief(plane.track); }, align: 'right' }; cols.msgs = { text: 'Messages', sort: function () { sortBy('msgs', compareNumeric, function(x) { return x.messages; }); }, value: function(plane) { return plane.messages; }, align: 'right' }; cols.seen = { text: 'Seen', sort: function () { sortBy('seen', compareNumeric, function(x) { return x.seen; }); }, value: function(plane) { return plane.seen.toFixed(0); }, align: 'right' }; cols.rssi = { text: 'RSSI', sort: function () { sortBy('rssi', compareNumeric, function(x) { return x.rssi; }); }, value: function(plane) { return (plane.rssi != null ? plane.rssi.toFixed(1) : ""); }, align: 'right' }; cols.lat = { text: 'Latitude', sort: function () { sortBy('lat', compareNumeric, function(x) { return (x.position !== null ? x.position[1] : null); }); }, value: function(plane) { return (plane.position != null ? plane.position[1].toFixed(4) : ""); }, align: 'right' }; cols.lon = { text: 'Longitude', sort: function () { sortBy('lon', compareNumeric, function(x) { return (x.position !== null ? x.position[0] : null); }); }, value: function(plane) { return (plane.position != null ? plane.position[0].toFixed(4) : ""); }, align: 'right' }; cols.data_source = { text: 'Source', sort: function () { sortBy('data_source', compareNumeric, function(x) { return x.getDataSourceNumber(); } ); }, value: function(plane) { return format_data_source(plane.getDataSource()); }, align: 'right' }; cols.military = { text: 'Mil.', sort: function () { sortBy('military', compareAlpha, function(x) { return (x.military ? 'yes' : 'no'); } ); }, value: function(plane) { return (plane.military ? 'yes' : 'no'); }, align: 'right' }; cols.wd = { text: 'Wind D.', sort: function () { sortBy('wd', compareNumeric, function(x) { return x.wd; }); }, value: function(plane) { return plane.wd != null ? (plane.wd + '°') : ''; }, align: 'right' }; cols.ws = { text: 'Wind S.', sort: function () { sortBy('ws', compareNumeric, function(x) { return x.ws; }); }, value: function(plane) { return format_speed_brief(plane.ws, DisplayUnits); }, align: 'right', header: function () { return 'Wind' + NBSP + '(' + get_unit_label("speed", DisplayUnits) + ')'; }, }; const colsEntries = Object.entries(cols); for (let i in colsEntries) { let key = colsEntries[i][0]; let value = colsEntries[i][1]; value.id = key; value.text = value.text ? value.text : ""; value.header = value.header ? value.header : function() { return value.text; }; value.hStyle = value.hStyle ? value.hStyle : ""; if (!value.td) value.td = value.align ? ('') : ''; } 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(); initializing = false; } planeMan.redraw = function () { activeCols = []; for (let i in columns) { let col = columns[i]; if (col.visible || !mapIsVisible) { activeCols.push(col); } } for (let i = 0; i < 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; planeMan.refresh(); } planeMan.setColumnVis = function (col, visible) { cols[col].visible = visible; if (!initializing) planeMan.redraw(); } // Refreshes the larger table of all the planes planeMan.refresh = function () { if (initializing) return; const ctime = false; // gets enabled for debugging table refresh speed // globeTableLimit = 1000; for testing performance ctime && console.time("planeMan.refresh()"); 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()"); } // // ---- table sorting begin ---- // let sortId = ''; let sortCompare = null; let sortExtract = null; let sortAscending = true; function sortFunction(x,y) { const xv = x._sort_value; const yv = y._sort_value; // always sort missing values at the end, regardless of // ascending/descending sort if (xv == null && yv == null) return x._sort_pos - y._sort_pos; if (xv == null) return 1; if (yv == null) return -1; const c = sortAscending ? sortCompare(xv,yv) : sortCompare(yv,xv); if (c !== 0) return c; return x._sort_pos - y._sort_pos; } function resortTable(pList) { if (!sortExtract) return; if (globeIndex) { // don't presort for globeIndex } // presort by dataSource else if (sortId == "sitedist") { for (let i = 0; i < pList.length; ++i) { pList[i]._sort_pos = i; } pList.sort(function(x,y) { const a = x.getDataSourceNumber(); const b = y.getDataSourceNumber(); if (a == b) return (x._sort_pos - y._sort_pos); return (a-b); }); } // or distance else if (sortId == "data_source") { pList.sort(function(x,y) { return (x.sitedist - y.sitedist); }); } // or longitude else { pList.sort(function(x,y) { return (x.position ? x.position[0] : 500) - (y.position ? y.position[0] : 500); }); } // number the existing rows so we can do a stable sort // regardless of whether sort() is stable or not. // Also extract the sort comparison value. if (globeIndex) { for (let i = 0; i < pList.length; ++i) { pList[i]._sort_pos = pList[i].numHex; pList[i]._sort_value = sortExtract(pList[i]); } } else { for (let i = 0; i < pList.length; ++i) { pList[i]._sort_pos = i; pList[i]._sort_value = sortExtract(pList[i]); } } pList.sort(sortFunction); // In multiSelect put selected planes on top, do a stable sort! if (multiSelect) { for (let i = 0; i < pList.length; ++i) { pList[i]._sort_pos = i; } pList.sort(function(x,y) { if (x.selected && y.selected) { return (x._sort_pos - y._sort_pos); } if (x.selected) return -1; if (y.selected) return 1; return (x._sort_pos - y._sort_pos); }); } } function sortBy(id, sc, se) { if (id != 'data_source' && grouptype_checkbox) { jQuery('#grouptype_checkbox').removeClass('settingsCheckboxChecked'); grouptype_checkbox = false; } else if (id == 'data_source' && !grouptype_checkbox) { jQuery('#grouptype_checkbox').addClass('settingsCheckboxChecked'); grouptype_checkbox = true; } if (id === sortId) { sortAscending = !sortAscending; g.planesOrdered.reverse(); // this correctly flips the order of rows that compare equal } else { sortAscending = true; } sortId = id; sortCompare = sc; sortExtract = se; planeMan.refresh(); } // // ---- table sorting end ---- // function createColumnToggles() { const prefix = 'dd_'; const sortableColumns = jQuery('#sortableColumns').sortable({ update: function (event, ui) { const order = []; jQuery('#sortableColumns li').each(function (e) { order.push(jQuery(this).attr('id').replace(prefix, '')); }); loStore['columnOrder'] = JSON.stringify(order); columns = createOrderedColumns(); planeMan.redraw(); } }); for (let col of columns) { sortableColumns.append(`
  • `); 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 && 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() { if (SitePosition) { CenterLon = SiteLon; CenterLat = SiteLat; } else { CenterLon = DefaultCenterLon; CenterLat = DefaultCenterLat; } // Reset loStore values and map settings loStore['CenterLat'] = CenterLat loStore['CenterLon'] = CenterLon //loStore['zoomLvl'] = zoomLvl = DefaultZoomLvl; // Set and refresh //OLMap.getView().setZoom(zoomLvl); OLMap.getView().setCenter(ol.proj.fromLonLat([CenterLon, CenterLat])); OLMap.getView().setRotation(mapOrientation); //selectPlaneByHex(null,false); jQuery("#update_error").css('display','none'); runAfterLoad(geoFindMe); } function updateMapSize() { if (OLMap) OLMap.updateSize(); } function expandSidebar(e) { e.preventDefault(); jQuery("#map_container").hide() mapIsVisible = false; jQuery("#toggle_sidebar_control").hide(); jQuery("#splitter").hide(); jQuery("#shrink_sidebar_button").show(); jQuery("#sidebar_container").width("100%"); TAR.planeMan.redraw(); updateMapSize(); adjustInfoBlock(); } function showMap() { jQuery('#sidebar_container').width(loStore['sidebar_width']).css('margin-left', '0'); jQuery("#map_container").show() mapIsVisible = true; jQuery("#toggle_sidebar_control").show(); jQuery("#splitter").show(); jQuery("#shrink_sidebar_button").hide(); TAR.planeMan.redraw(); updateMapSize(); } let selectedPhotoCache = null; function setPhotoHtml(source) { if (selectedPhotoCache == source) return; //console.log(source + ' ' + selectedPhotoCache); selectedPhotoCache = source; jQuery('#selected_photo').html(source); } function adjustInfoBlock() { if (wideInfoBlock ) { infoBlockWidth = baseInfoBlockWidth + 40; } else { infoBlockWidth = baseInfoBlockWidth; } jQuery('#selected_infoblock').css("width", infoBlockWidth * globalScale + 'px'); jQuery('.ol-scale-line').css('left', (infoBlockWidth * globalScale + 8) + 'px'); jQuery('#replayBar').css('left', (infoBlockWidth * globalScale + 8) + 'px'); if (SelectedPlane && toggles['enableInfoblock'].state) { if (!mapIsVisible) jQuery("#sidebar_container").css('margin-left', '140pt'); //jQuery('#sidebar_canvas').css('margin-bottom', jQuery('#selected_infoblock').height() + 'px'); // if (mapIsVisible && document.getElementById('map_canvas').clientWidth < parseFloat(jQuery('#selected_infoblock').css('width')) * 3) { jQuery('#selected_infoblock').css('height', '290px'); jQuery('#selected_typedesc').parent().parent().hide(); jQuery('#credits').css('bottom', '295px'); jQuery('#credits').css('left', '5px'); } else { jQuery('#selected_infoblock').css('height', '100%'); jQuery('#credits').css('bottom', ''); jQuery('#credits').css('left', ''); } jQuery('#selected_infoblock').show(); } else { if (!mapIsVisible) jQuery("#sidebar_container").css('margin-left', '0'); //jQuery('#sidebar_canvas').css('margin-bottom', 0); jQuery('.ol-scale-line').css('left', '8px'); jQuery('#replayBar').css('left', '0px'); jQuery('#credits').css('bottom', ''); jQuery('#credits').css('left', ''); jQuery('#selected_infoblock').hide(); } let photoWidth = document.getElementById('photo_container').clientWidth; let refWidth = infoBlockWidth * globalScale - 29; if (Math.abs(photoWidth / refWidth - 1) > 0.05) photoWidth = refWidth; jQuery('#airplanePhoto').css("width", photoWidth + 'px'); jQuery('#selected_photo').css("width", photoWidth + 'px'); if (showPictures) { if (planespottersAPI || planespottingAPI) { jQuery('#photo_container').css('height', photoWidth * 0.883 + 'px'); } else { jQuery('#photo_container').css('height', '40px'); } } } function initializeUnitsSelector() { // Get display unit preferences from local storage if (!loStore.getItem('displayUnits')) { loStore['displayUnits'] = 'nautical'; } DisplayUnits = loStore['displayUnits']; // Initialize drop-down jQuery('#units_selector') .val(DisplayUnits) .on('change', onDisplayUnitsChanged); jQuery(".altitudeUnit").text(get_unit_label("altitude", DisplayUnits)); jQuery(".speedUnit").text(get_unit_label("speed", DisplayUnits)); jQuery(".distanceUnit").text(get_unit_label("distance", DisplayUnits)); jQuery(".verticalRateUnit").text(get_unit_label("verticalRate", DisplayUnits)); } function onDisplayUnitsChanged(e) { loStore['displayUnits'] = DisplayUnits = e.target.value; TAR.altitudeChart.render(); // Update filters updateAltFilter(); // Refresh data refreshFilter(); // Draw range rings if (siteCircleLayer.getVisible()) { drawSiteCircle(); } // Reset map scale line units OLMap.getControls().forEach(function(control) { if (control instanceof ol.control.ScaleLine) { control.setUnits(DisplayUnits); } }); jQuery(".altitudeUnit").text(get_unit_label("altitude", DisplayUnits)); jQuery(".speedUnit").text(get_unit_label("speed", DisplayUnits)); jQuery(".distanceUnit").text(get_unit_label("distance", DisplayUnits)); jQuery(".verticalRateUnit").text(get_unit_label("verticalRate", DisplayUnits)); TAR.planeMan.redraw(); remakeTrails(); refreshSelected(); } function onFilterByAltitude(e) { e.preventDefault(); jQuery("#altitude_filter_min").blur(); jQuery("#altitude_filter_max").blur(); updateAltFilter(); refreshFilter(); } function filterGroundVehicles(switchFilter) { if (typeof loStore['groundVehicleFilter'] === 'undefined') { loStore['groundVehicleFilter'] = 'not_filtered'; } let groundFilter = loStore['groundVehicleFilter']; if (switchFilter === true) { groundFilter = (groundFilter === 'not_filtered') ? 'filtered' : 'not_filtered'; } if (groundFilter === 'not_filtered') { jQuery('#groundvehicle_filter').addClass('settingsCheckboxChecked'); } else { jQuery('#groundvehicle_filter').removeClass('settingsCheckboxChecked'); } loStore['groundVehicleFilter'] = groundFilter; PlaneFilter.groundVehicles = groundFilter; } function filterBlockedMLAT(switchFilter) { if (typeof loStore['blockedMLATFilter'] === 'undefined') { loStore['blockedMLATFilter'] = 'not_filtered'; } let blockedMLATFilter = loStore['blockedMLATFilter']; if (switchFilter === true) { blockedMLATFilter = (blockedMLATFilter === 'not_filtered') ? 'filtered' : 'not_filtered'; } if (blockedMLATFilter === 'not_filtered') { jQuery('#blockedmlat_filter').addClass('settingsCheckboxChecked'); } else { jQuery('#blockedmlat_filter').removeClass('settingsCheckboxChecked'); } loStore['blockedMLATFilter'] = blockedMLATFilter; PlaneFilter.blockedMLAT = blockedMLATFilter; } function buttonActive(id, state) { if (state) { jQuery(id).addClass('activeButton'); jQuery(id).removeClass('inActiveButton'); } else { jQuery(id).addClass('inActiveButton'); jQuery(id).removeClass('activeButton'); } } function toggleIsolation(state) { let prevState = onlySelected; if (showTrace && state !== "on") return; onlySelected = !onlySelected; if (state === "on") onlySelected = true; if (state === "off") onlySelected = false; buttonActive('#I', onlySelected); if (prevState != onlySelected) refreshFilter(); 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 { const dim = mapDimPercentage * (1 + 0.25 * toggles['darkerColors'].state) + layerExtraDim; const contrast = mapContrastPercentage * (1 + 0.1 * toggles['darkerColors'].state) + layerExtraContrast; if (dim > 0.0001) { evt.context.globalCompositeOperation = 'multiply'; evt.context.fillStyle = 'rgba(0,0,0,'+dim+')'; evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height); } else if (dim < -0.0001) { evt.context.globalCompositeOperation = 'screen'; console.log(evt.context.globalCompositeOperation); evt.context.fillStyle = 'rgba(255, 255, 255,'+(-dim)+')'; evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height); } if (contrast > 0.0001) { evt.context.globalCompositeOperation = 'overlay'; evt.context.fillStyle = 'rgba(0,0,0,'+contrast+')'; evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height); } else if (contrast < -0.0001) { evt.context.globalCompositeOperation = 'overlay'; evt.context.fillStyle = 'rgba(255, 255, 255,'+ (-contrast)+')'; evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height); } evt.context.globalCompositeOperation = 'source-over'; } catch (error) { console.error(error); } } function invertMap(evt){ const ctx=evt.context; ctx.globalCompositeOperation='difference'; ctx.fillStyle = "white"; ctx.globalAlpha = alpha; // alpha 0 = no effect 1 = full effect ctx.fillRect(0, 0, evt.ctx.canvas.width, ctx.canvas.height); } // // Altitude Chart begin // (function (global, jQuery, TAR) { let altitudeChart = TAR.altitudeChart = TAR.altitudeChart || {}; function createLegendGradientStops() { const mapOffsetToAltitude = [[0.033, 500], [0.066, 1000], [0.126, 2000], [0.19, 4000], [0.253, 6000], [0.316, 8000], [0.38, 10000], [0.59, 20000], [0.79, 30000], [1, 40000]]; let stops = ''; for (let i in mapOffsetToAltitude) { let map = mapOffsetToAltitude[i]; const color = altitudeColor(map[1]); stops += ''; } return stops; } function createLegendUrl(data) { jQuery(data).find('#linear-gradient').html(createLegendGradientStops()); const svg = jQuery('svg', data).prop('outerHTML'); return 'url("data:image/svg+xml;base64,' + global.btoa(svg) + '")'; } function loadLegend() { let baseLegend = (DisplayUnits === 'metric') ? 'images/alt_legend_m.svg' : 'images/alt_legend_ft.svg'; jQuery.get(baseLegend, function (data) { jQuery('#altitude_chart_button').css("background-image", createLegendUrl(data)); }); } altitudeChart.render = function () { if (toggles['altitudeChart'].state) { loadLegend(); jQuery('#altitude_chart').show(); } else { jQuery('#altitude_chart').hide(); } } altitudeChart.init = function () { 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() { enableLabels = !enableLabels; loStore['enableLabels'] = enableLabels; for (let key in g.planesOrdered) { g.planesOrdered[key].updateMarker(); } refreshFeatures(); buttonActive('#L', enableLabels); if (showTrace) remakeTrails(); } function toggleExtendedLabels(options) { if (isNaN(extendedLabels)) extendedLabels = 0; options = options || {}; if (!options.noIncrement) { extendedLabels++; } extendedLabels %= 4; //console.log(extendedLabels); loStore['extendedLabels'] = extendedLabels; for (let key in g.planesOrdered) { g.planesOrdered[key].updateMarker(); } buttonActive('#O', extendedLabels); } function toggleTrackLabels() { trackLabels = !trackLabels; loStore['trackLabels'] = trackLabels; remakeTrails(); buttonActive('#K', trackLabels); } function toggleMultiSelect(newState) { let prevState = multiSelect; multiSelect = !multiSelect; if (newState == "on") multiSelect = true; if (newState == "off") multiSelect = false; if (!multiSelect) { if (!SelectedPlane) toggleIsolation("off"); if (prevState != multiSelect) deselectAllPlanes("keepMain"); } buttonActive('#M', multiSelect); } function onJump(e) { toggleFollow(false); if (e) { e.preventDefault(); onJumpInput = jQuery("#jump_input").val(); jQuery("#jump_input").val(""); jQuery("#jump_input").blur(); } let coords = null; let airport = null; if (onJumpInput.indexOf(",") >= 0) { let values = onJumpInput.split(','); if (!values || values.length != 2) { showSearchWarning('Input format decimal coordinates: LATI.TUDE, LONGI.TUDE'); } coords = [parseFloat(values[0]), parseFloat(values[1])]; } else { airport = onJumpInput.trim().toUpperCase(); } if (airport) { if (!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 (zoomLvl >= 7) { fetchData({force: true}); } refreshFilter(); hideSearchWarning(); } else { showSearchWarning('Failed to find airport ' + airport); } } function hideSearchWarning() { const searchWarning = jQuery('#search_warning'); if (searchWarning.css('display') !== 'none') { searchWarning.hide('slow'); } } function showSearchWarning(message) { const searchWarning = jQuery('#search_warning'); searchWarning.text(message) if (searchWarning.css('display') === 'none') { searchWarning.show(); } //auto hide after 15 seconds setTimeout(() => hideSearchWarning(), 15000); } function onSearch(e) { e.preventDefault(); const searchTerm = jQuery("#search_input").val().trim(); jQuery("#search_input").val(""); jQuery("#search_input").blur(); let results = []; if (searchTerm) results = findPlanes(searchTerm, "byIcao", "byCallsign", "byReg", "byType", true); if (results.length > 0 && globeIndex) { toggleIsolation("on"); if (results.length < 100) { getTrace(null, null, {list: results}); } } return false; } function onSearchClear(e) { deselectAllPlanes(); toggleIsolation("off"); toggleMultiSelect("off"); jQuery("#search_input").val(""); jQuery("#search_input").blur(); } function 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 = `
    ` + '
    Filter by '+ this.name +':
    ' + `` + '' + `` + '
    ' ; 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' }); 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; zoomLvl = OLMap.getView().getZoom(); checkScale(); // small zoomstep, no need to change aircraft scaling if (!init && Math.abs(zoomLvl-zoomLvlCache) < 0.4) return; loStore['zoomLvl'] = zoomLvl; zoomLvlCache = zoomLvl; if (!init && showTrace) updateAddressBar(); checkPointermove(); } function checkScale() { if (zoomLvl > markerZoomDivide) { iconSize = markerBig; } else if (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 = "bold " + (12 * globalScale * labelScale) + "px/" + (14 * globalScale * labelScale) + "px Tahoma, Verdana, Helvetica, sans-serif"; checkScale(); setLineWidth(); if (!init) { refreshFeatures(); refreshSelected(); refreshHighlighted(); remakeTrails(); } } function checkPointermove() { if ((webgl || zoomLvl > 5.5) && enableMouseover && !onMobile) { OLMap.on('pointermove', onPointermove); } else { OLMap.un('pointermove', onPointermove); removeHighlight(); } } function changeCenter(init) { const rawCenter = OLMap.getView().getCenter(); const center = ol.proj.toLonLat(rawCenter); const centerChanged = (Math.abs(center[1] - CenterLat) > 0.000001 || Math.abs(center[0] - CenterLon) > 0.000001); if (!init && !centerChanged) { return; } loStore['CenterLon'] = CenterLon = center[0]; loStore['CenterLat'] = CenterLat = center[1]; if (!init) { updateAddressBar(); } if (rawCenter[0] < OLProjExtent[0] || rawCenter[0] > OLProjExtent[2]) { OLMap.getView().setCenter(ol.proj.fromLonLat(center)); 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; const zoom = OLMap.getView().getZoom(); const center = ol.proj.toLonLat(OLMap.getView().getCenter()); const ts = new Date().getTime(); if ( checkMoveZoom != zoom || checkMoveCenter[0] != center[0] || checkMoveCenter[1] != center[1] ) { checkMoveDone = 0; if (FollowSelected) { checkFollow(); } active(); lastMovement = ts; } checkMoveZoom = zoom; checkMoveCenter[0] = center[0]; checkMoveCenter[1] = center[1]; changeZoom(); changeCenter(); const elapsed = Math.abs(ts - lastMovement); if (!checkMoveDone && heatmap && elapsed > 300) { if (!heatmap.manualRedraw) drawHeatmap(); checkMoveDone = 1; } if (elapsed > 500 || (!onMobile && elapsed > 45)) { checkRefresh(); } fetchData(); } function getZoom() { return OLMap.getView().getZoom(); } function getCenter() { return ol.proj.toLonLat(OLMap.getView().getCenter()); } let lastRefresh = 0; let refreshZoom, refreshCenter; function checkRefresh() { if (showTrace) return; if (triggerRefresh) { refresh(); return; } const center = getCenter(); const zoom = getZoom(); if (zoom != refreshZoom || !refreshCenter || center[0] != refreshCenter[0] || center[1] != refreshCenter[1]) { const ts = new Date().getTime(); const elapsed = Math.abs(ts - lastRefresh); let num = Math.min(1500, Math.max(250, TrackedAircraftPositions / 300 * 250)); if (elapsed > num) { refresh(); } } } function refresh(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(); //console.time("refreshTable"); TAR.planeMan.refresh(); //console.timeEnd("refreshTable"); mapRefresh(redraw); 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 || 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) { const hex = evt.map.forEachFeatureAtPixel(evt.pixel, function(feature, layer) { return feature.hex; }, { layerFilter: function(layer) { return (layer == iconLayer || layer == webglLayer); }, hitTolerance: 5 * globalScale, } ); if (HighlightedPlane && hex == HighlightedPlane.icao) return; //clearTimeout(pointerMoveTimeout); if (hex) { HighlightedPlane = 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); } } if (urlIcaos.length > 0) { const icaos = urlIcaos; if (!usp.has('noIsolation')) toggleIsolation("on"); if (icaos.length > 1) { toggleMultiSelect("on"); //follow = false; } for (let i = 0; i < icaos.length; i++) { const icao = icaos[i]; console.log('Selected ICAO id: '+ icao + ' traceDate: ' + traceDateString); let options = {follow: follow, noDeselect: true}; if (traceDate != null) { let newPlane = 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)) ) { if (globeIndex) { if (plane.inView) results.push(plane); } else { if (plane.checkVisible()) results.push(plane); } } } } if (results.length > 1) { toggleMultiSelect("on"); for (let i in results) { select(results[i], {}); results[i].updateTick(true); sp = SelectedPlane = null; } showWarnings && hideSearchWarning(); } else if (results.length == 1) { selectPlaneByHex(results[0].icao, {noDeselect: true, follow: true}); console.log("query selected: " + queries); showWarnings && hideSearchWarning(); } else { console.log("No match found for query: " + queries); let foundByHex = 0; if (globeIndex) { for (let i in queries) { const query = queries[i]; if (query.toLowerCase().match(/~?[a-f,0-9]{6}/)) { console.log("maybe it's an icao, let's try to fetch the history for it!"); selectPlaneByHex(query, {noDeselect: true, follow: true}) && foundByHex++ } } } if (foundByHex === 0 && showWarnings) { if (globeIndex) { showSearchWarning("No match found in current view: " + queries); } else { showSearchWarning("No match found for query: " + queries); } } } return results; } function trailReaper() { for (let i in 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; function updateAddressBar() { if (!window.history || !window.history.replaceState) return; if (heatmap || pTracks || !CenterLat || uuid) return; let string = ''; if (SelPlanes.length > 0) { string += '?icao=' + SelPlanes.map((s) => encodeURIComponent(s.icao)).join(',') } else if (replay) { string += '?replay='; string += zDateString(replay.ts); string += '-' + replay.ts.getUTCHours().toString().padStart(2,'0'); string += ':' + replay.ts.getUTCMinutes().toString().padStart(2,'0'); } if (showTrace || replay) { string += (string ? '&' : '?'); string += 'lat=' + CenterLat.toFixed(3) + '&lon=' + CenterLon.toFixed(3) + '&zoom=' + zoomLvl.toFixed(1); } if (SelPlanes.length > 0 && (showTrace || replay)) { string += (string ? '&' : '?'); string += 'showTrace=' + traceDateString; if (legSel != -1) string += '&leg=' + (legSel + 1); if (traceOpts.startHours != null) { string += '&startTime='; string += traceOpts.startHours.toString().padStart(2, '0'); string += ':' + traceOpts.startMinutes.toString().padStart(2, '0'); if (traceOpts.startSeconds) { string += ':' + traceOpts.startSeconds.toString().padStart(2, '0'); } } if (traceOpts.endHours != null) { string += '&endTime='; string += traceOpts.endHours.toString().padStart(2, '0'); string += ':' + traceOpts.endMinutes.toString().padStart(2, '0'); if (traceOpts.endSeconds) { string += ':' + traceOpts.endSeconds.toString().padStart(2, '0'); } } if (trackLabels) { string += '&trackLabels'; if (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 (!updateAddressBarPushed) { // make sure we keep the thing we clicked on first in the browser history window.history.pushState("object or string", "Title", string); updateAddressBarPushed = true; } else { // but don't create a new history entry for every plane we click on window.history.replaceState("object or string", "Title", string); } } function refreshInt() { let refresh = RefreshInterval; if (uuid) return 5050; // handle non globe case if (!globeIndex) { return refresh; } // handle globe case if (reApi && (binCraft || zstd)) { refresh = RefreshInterval * lastRequestSize / 35000; let extent = getViewOversize(1.03); let min = 0.7; let max = 7; if (zoomLvl < 5) { min += Math.min(1, (5 - zoomLvl) / 4); } if (refresh < RefreshInterval * min) { refresh = RefreshInterval * min; } if (refresh > RefreshInterval * max) { refresh = RefreshInterval * max; } if (onlySelected && SelPlanes.length == 0 && reApi) { // no aircraft selected, none shown refresh = RefreshInterval * max * 2; } if (!FollowSelected && lastRequestBox != requestBoxString()) { refresh = Math.min(RefreshInterval, refresh / 4); } } if (!reApi && binCraft && globeIndex && onlyMilitary && OLMap.getView().getZoom() < 5.5) { refresh = 5000; } let inactive = getInactive(); 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 (adsbexchange && 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); lastRefreshInt = refresh; return refresh; } function toggleShowTrace() { showTrace = !showTrace; if (showTrace) { jQuery("#selected_showTrace_hide").hide(); toggleFollow(false); showTraceWasIsolation = onlySelected; toggleIsolation("on"); shiftTrace(); } 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: zoomLvl,}); } jQuery('#history_collapse').toggle(); jQuery('#show_trace').toggleClass('active'); } function legShift(offset, plane) { if(!offset) offset = 0; if (!plane) { legSel += offset; for (let i in SelPlanes) { legShift(offset, SelPlanes[i]); } return; } if (offset != 0) traceOpts.showTime = null; if (!multiSelect && !plane.fullTrace) { jQuery('#leg_sel').text('No Data available for\n' + traceDateString); jQuery('#trace_time').text('UTC:\n'); } if (!plane.fullTrace) { plane.processTrace(); return; } let trace = plane.fullTrace.trace; let legStart = null; let legEnd = null; let count = 0; for (let i = 1; i < trace.length; i++) { let timestamp = trace[i][0]; if (traceOpts.startStamp != null && timestamp < traceOpts.startStamp) { continue; } if (traceOpts.endStamp != null && timestamp > traceOpts.endStamp) break; if (legStart == null) { legStart = i; i++; if (i >= trace.length) break; } if (trace[i][6] & 2) { count++; } } if (legSel < -1) legSel = count; if (legSel > count) legSel = -1; if (legSel == -1) { jQuery('#leg_sel').text('Legs: All'); traceOpts.legStart = null; traceOpts.legEnd = null; plane.processTrace(); updateAddressBar(); return; } count = 0; for (let i = legStart + 1; i < trace.length; i++) { let timestamp = trace[i][0]; if (traceOpts.endStamp != null && timestamp > traceOpts.endStamp) break; if (trace[i][6] & 2) { if (count == legSel - 1) legStart = i; if (count == legSel) legEnd = i; // exclusive count++; } } jQuery('#leg_sel').text('Leg: ' + (legSel + 1)); traceOpts.legStart = legStart; traceOpts.legEnd = legEnd; plane.processTrace(); updateAddressBar(); } function setTraceDate(options) { options = options || {}; let numbers = options.string ? options.string.split('-') : []; if (numbers.length == 3) { traceDate = new Date(); traceDate.setUTCFullYear(numbers[0]); traceDate.setUTCMonth(numbers[1] - 1, numbers[2]); } else if (options.ts) { traceDate = new Date(options.ts); } else { return null; } traceDate.setUTCHours(0); traceDate.setUTCMinutes(0); traceDate.setUTCSeconds(0); let tomorrow = (new Date()).getTime() + 86400e3; if (traceDate.getTime() > tomorrow) { traceDate = new Date(tomorrow); } traceDateString = zDateString(traceDate); return traceDate; } function shiftTrace(offset) { if (traceRate > 180) { jQuery('#leg_sel').text('Slow down! ...'); return; } // reset some traceOpts stuff (important) traceOpts.startStamp = null; traceOpts.endStamp = null; traceOpts.showTimeEnd = null; traceOpts.showTime = null; jQuery('#leg_sel').text('Loading ...'); if (!traceDate || offset == "today") { setTraceDate({ ts: new Date().getTime() }); } else if (offset) { setTraceDate({ ts: traceDate.getTime() + offset * 86400 * 1000 }); } //jQuery('#trace_date').text('UTC day:\n' + traceDateString); jQuery("#histDatePicker").datepicker('setDate', traceDateString); for (let i in SelPlanes) { selectPlaneByHex(SelPlanes[i].icao, {noDeselect: true, zoom: zoomLvl}); } updateAddressBar(); } function setLineWidth() { newWidth = lineWidth * Math.pow(2, globalScale) / 2 * globalScale estimateStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#808080', width: 1.2 * newWidth, }) }); estimateStyleSlim = new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#808080', width: 0.4 * newWidth, }) }); badLine = new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#FF0000', width: 2 * newWidth, }) }); badLineMlat = new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#FFA500', width: 2 * newWidth, }) }); badDot = new ol.style.Style({ image: new ol.style.Circle({ radius: 3.5 * newWidth, fill: new ol.style.Fill({ color: '#FF0000', }) }), }); badDotMlat = new ol.style.Style({ image: new ol.style.Circle({ radius: 3.5 * newWidth, fill: new ol.style.Fill({ color: '#FFA500', }) }), }); labelFill = new ol.style.Fill({color: 'white' }); blackFill = new ol.style.Fill({color: 'black' }); labelStroke = new ol.style.Stroke({color: 'rgba(0,0,0,0.7', width: 4 * globalScale}); labelStrokeNarrow = new ol.style.Stroke({color: 'rgba(0,0,0,0.7', width: 2.5 * globalScale}); bgFill = new ol.style.Stroke({color: 'rgba(0,0,0,0.25'}); } let lastCallLocationChange = 0; function onLocationChange(position) { lastCallLocationChange = new Date().getTime(); changeCenter(); const moveMap = (Math.abs(SiteLat - CenterLat) < 0.000001 && Math.abs(SiteLon - CenterLon) < 0.000001); SiteLat = DefaultCenterLat = position.coords.latitude; SiteLon = DefaultCenterLon = position.coords.longitude; SitePosition = [SiteLon, SiteLat]; drawSiteCircle(); createLocationDot(); if (moveMap || lockDotCentered) { OLMap.getView().setCenter(ol.proj.fromLonLat([SiteLon, SiteLat])); } console.log('Changed Site Location to: '+ SiteLat +', ' + SiteLon); //followRandomPlane(); //togglePersistence(); } function logArg(error) { console.log(error); } let watchPositionId; let pollPositionSeconds = 10; function pollPositionInterval() { if (!updateLocation || !geoFindEnabled()) { return; } // interval position polling every half minute for browsers that are shit //console.trace(); clearInterval(timers.pollPosition); timers.pollPosition = window.setInterval(function() { // if we recently got a new location via watchPosition(), don't query if (new Date().getTime() - lastCallLocationChange < pollPositionSeconds * 0.85 * 1000) return; if (tabHidden) return; console.log('pollPositionInterval: querying position'); const geoposOptions = { enableHighAccuracy: false, timeout: pollPositionSeconds * 1000, maximumAge: pollPositionSeconds * 1000 , }; navigator.geolocation.getCurrentPosition(function(position) { onLocationChange(position); }, logArg, geoposOptions); }, pollPositionSeconds * 1000); } function watchPosition() { if (watchPositionId != null) { navigator.geolocation.clearWatch(watchPositionId); } if (!updateLocation || !geoFindEnabled()) { return; } const geoposOptions = { enableHighAccuracy: false, timeout: Infinity, maximumAge: 25 * 1000, }; console.log("watching position"); watchPositionId = navigator.geolocation.watchPosition(function(position) { onLocationChange(position); pollPositionSeconds = 60; }, logArg, geoposOptions); pollPositionInterval(); } let geoFindInterval = null; function geoFindMe() { //console.trace(); if (!geoFindEnabled()) { initSitePos(); return; } function success(position) { SiteLat = DefaultCenterLat = position.coords.latitude; SiteLon = DefaultCenterLon = position.coords.longitude; if (loStore['geoFindMeFirstVisit'] != 'no' && !(usp.has("lat") && usp.has("lon"))) { OLMap.getView().setCenter(ol.proj.fromLonLat([SiteLon, SiteLat])); loStore['geoFindMeFirstVisit'] = 'no'; siteCircleLayer.setVisible(true); } initSitePos(); console.log('Location from browser: '+ SiteLat +', ' + SiteLon); { // always update user location every 15 minutes clearInterval(geoFindInterval); geoFindInterval = window.setInterval(function() { if (tabHidden) return; const geoposOptions = { enableHighAccuracy: false, timeout: 15 * 60 * 1000, maximumAge: 5 * 60 * 1000 , }; console.log('geoFindInterval: querying position'); navigator.geolocation.getCurrentPosition(onLocationChange, logArg, geoposOptions); }, 15 * 60 * 1000); } } function error() { console.log("Unable to query location."); initSitePos(); } if (!navigator.geolocation) { console.log('Geolocation is not supported by your browser'); } else { // change SitePos on location change console.log('Locating…'); const geoposOptions = { enableHighAccuracy: false, timeout: Infinity, maximumAge: 300 * 1000, }; navigator.geolocation.getCurrentPosition(success, error, geoposOptions); } } let initSitePosFirstRun = true; function initSitePos() { // Set SitePosition if (SiteLat != null && SiteLon != null) { SitePosition = [SiteLon, SiteLat]; // Add home marker if requested drawSiteCircle(); createLocationDot(); } else { TAR.planeMan.setColumnVis('distance', false); } if (initSitePosFirstRun && SitePosition) { initSitePosFirstRun = false; const sortBy = usp.get('sortBy'); if (sortBy) { TAR.planeMan.cols[sortBy].sort(); if (usp.has('sortByReverse')) { TAR.planeMan.cols[sortBy].sort(); } } else if (SitePosition) { TAR.planeMan.cols.distance.sort(); } else { TAR.planeMan.cols.altitude.sort(); TAR.planeMan.cols.altitude.sort(); } } } /* function drawAlt() { processAircraft({hex: 'c0ffee', }); let plane = 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); } } } let actualOutlineLayer; let actualOutlineFeatures; let actualOutlineStyle; function drawOutlineJson() { if (!receiverJson || !receiverJson.outlineJson) return; let request = jQuery.ajax({ url: 'data/outline.json', cache: false, dataType: 'json' }); request.done(function(data) { actualOutlineFeatures.clear(); let points; if (data.actualRange && data.actualRange.last24h) { points = data.actualRange.last24h.points; } else { points = data.points; } if (!points || !points.length) return; let geom = null; let lastLon = null; for (let j = 0; j < points.length + 1; ++j) { const k = j % points.length; const lat = points[k][0]; const lon = points[k][1]; const proj = ol.proj.fromLonLat([lon, lat]); if (!geom || (lastLon && Math.abs(lon - lastLon) > 270)) { geom = new ol.geom.LineString([proj]); actualOutlineFeatures.addFeature(new ol.Feature(geom)); } else { geom.appendCoordinate(proj); } lastLon = lon; } }); request.fail(function() { // no rings available, do nothing }); } function gotoTime(timestamp) { clearTimeout(traceOpts.showTimeout); if (timestamp) { traceOpts.showTime = timestamp; traceOpts.animate = false; } if (!traceOpts.animate) { legShift(0); } else { let marker = SelectedPlane.glMarker || SelectedPlane.marker; if (marker) { traceOpts.animatePos[0] += (traceOpts.animateToLon - traceOpts.animateFromLon) / traceOpts.animateSteps; traceOpts.animatePos[1] += (traceOpts.animateToLat - traceOpts.animateFromLat) / traceOpts.animateSteps; SelectedPlane.updateMarker(); } if (--traceOpts.animateCounter == 1) { traceOpts.animate = false; traceOpts.showTime = traceOpts.showTimeEnd; console.log(traceOpts.showTime); } traceOpts.animateStepTime = traceOpts.animateRealtime / traceOpts.replaySpeed / traceOpts.animateSteps; traceOpts.showTimeout = setTimeout(gotoTime, traceOpts.animateStepTime); } } function checkFollow() { if (!FollowSelected) return false; if (!SelectedPlane || !SelectedPlane.position) { toggleFollow(false); return false; } const center = OLMap.getView().getCenter(); let proj = SelectedPlane.proj; if (!proj) { return false; } if (Math.abs(center[0] - proj[0]) > 1 || Math.abs(center[1] - proj[1]) > 1) { toggleFollow(false); return false; } return true; } function everySecond() { if (traceRate > 0) traceRate = traceRate * 0.985 - 1; updateIconCache(); } let getTraceTimeout = null; function getTrace(newPlane, hex, options) { if (options.list) { newPlane = options.list.pop() if (!newPlane) { return; } hex = newPlane.icao; } if (!newPlane) { newPlane = 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(); if ((showTrace || replay) && !(today.getTime() > traceDate.getTime() && today.getTime() < traceDate.getTime() + (24 * 3600 + 60 * 60) * 1000)) { let dateString = traceDateString || zDateString(today); URL1 = null; URL2 = 'globe_history/' + dateString.replace(/-/g, '/') + '/traces/' + hex.slice(-2) + '/trace_full_' + hex + '.json'; traceRate += 3; } else { URL1 = 'data/traces/'+ hex.slice(-2) + '/trace_recent_' + hex + '.json'; URL2 = 'data/traces/'+ hex.slice(-2) + '/trace_full_' + hex + '.json'; traceRate += 2; } if (showTrace && trace_hist_only) { let dateString = traceDateString || zDateString(today); URL2 = 'globe_history/' + dateString.replace(/-/g, '/') + '/traces/' + hex.slice(-2) + '/trace_full_' + hex + '.json'; } traceOpts.follow = (options.follow == true); if (showTrace) { //console.log(today.toUTCString() + ' ' + traceDate.toUTCString()); if (traceOpts.startHours == null || traceOpts.startHours < 0) traceOpts.startStamp = traceDate.getTime() / 1000; else traceOpts.startStamp = traceDate.getTime() / 1000 + traceOpts.startHours * 3600 + traceOpts.startMinutes * 60 + traceOpts.startSeconds; if (traceOpts.endHours == null || traceOpts.endHours >= 24) traceOpts.endStamp = traceDate.getTime() / 1000 + 24 * 3600; else traceOpts.endStamp = traceDate.getTime() / 1000 + traceOpts.endHours * 3600 + traceOpts.endMinutes * 60 + traceOpts.endSeconds; } if (newPlane && (showTrace || showTraceExit)) { newPlane.trace = []; newPlane.recentTrace = null; newPlane.fullTrace = null; } //console.log(URL2); //options = JSON.parse(JSON.stringify(options)); options.plane = `${newPlane.icao}`; options.defer = jQuery.Deferred(); if (URL1 && !options.onlyFull) { jQuery.ajax({ url: `${URL1}`, dataType: 'json', options: options, }) .done(function(data) { const options = this.options; const plane = 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 i = 4 * indexes[k][offsets[k]]; if (points[i] == 0xe7f7c9d) i += 4; if (i < 0) { console.log('wat ' + i); break; } for (; i < points.length; i += 4) { if (points[i] == 0xe7f7c9d) break; let lat = points[i+1]; if (lat > maxLat || lat < minLat) continue; lat /= 1000000; let lon = points[i + 2] / 1000000; let pos = [lon, lat]; if (!inView(pos, ext)) continue; let alt = points[i + 3] & 65535; if (alt & 32768) alt |= -65536; if (alt == -123) alt = 'ground'; else alt *= 25; let gs = points[i + 3] >> 16; if (gs == -1) gs = null; else gs /= 10; if (PlaneFilter.enabled && altFiltered(alt)) continue; pointCount++; //console.log(pos); alt = calcAltitudeRounded(alt); let projHere = ol.proj.fromLonLat(pos); let style = lineStyleCache[alt]; let hsl = altitudeColor(alt); hsl[1] = hsl[1] * 0.85; hsl[2] = hsl[2] * 0.8; if (!style) { let col; if (heatmap.alpha == null) col = hslToRgb(hsl); else col = hslToRgb(hsl, heatmap.alpha); style = new ol.style.Style({ image: new ol.style.Circle({ radius: heatmap.radius * globalScale, fill: new ol.style.Fill({ color: col, }), }), zIndex: i, }); lineStyleCache[alt] = style; } let feat = new ol.Feature(new ol.geom.Point(projHere)); if (webgl) { let rgb = hslToRgb(hsl, 'array'); feat.set('r', rgb[0]); feat.set('g', rgb[1]); feat.set('b', rgb[2]); } else { feat.setStyle(style); } features.push(feat); //console.log(alt); } offsets[k] += 1; } } if (iterations >= maxIter) console.log("drawHeatmap: MAX_ITERATIONS!"); //console.log(setSize(done)); console.log("files: " + myPoints.length + ", points drawn: " + pointCount); if (heatmap.real) { realHeatFeatures.addFeatures(features); } else { if (webgl) { webglFeatures.addFeatures(features); } else { for (let i = 0; i < heatFeaturesSpread; i++) { heatFeatures[i].addFeatures(features.splice(0, pointCount / heatFeaturesSpread + 1)); //console.log(features.length); } } } console.timeEnd("drawHeat"); jQuery("#loader").addClass("hidden"); } function currentExtent(factor) { let size = OLMap.getSize(); if (factor != null) size = [size[0] * factor, size[1] * factor]; return myExtent(OLMap.getView().calculateExtent(size)); } function replayDefaults(ts) { jQuery("#replayPlay").html("Pause"); return { playing: true, ts: ts, ival: 60 * 1000, speed: 30, dateText: zDateString(ts), hours: ts.getUTCHours(), minutes: ts.getUTCMinutes(), }; } function replayClear() { clearTimeout(refreshId); reaper(true); refreshFilter(); 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(); setTraceDate({ts: ts}); 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); setTraceDate({ts: ts}); 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 replayOnSliderMove() { clearTimeout(refreshId); let date = new Date(replay.dateText); date.setUTCHours(Number(replay.hours)); date.setUTCMinutes(Number(replay.minutes)); replay.seconds = 0; date.setUTCSeconds(Number(replay.seconds)); if (true || utcTimesHistoric) { jQuery("#replayDateHint").html("Date: " + zDateString(date)); jQuery("#replayTimeHint").html("Time: " + zuluTime(date) + NBSP + 'Z'); } else { jQuery("#replayDateHint").html("Date: " + lDateString(date)); jQuery("#replayTimeHint").html("Time: " + localTime(date) + NBSP + TIMEZONE); } } let replayJumpEnabled = true; function replayJump() { if (!showingReplayBar) return; if (!replayJumpEnabled) return; let date = new Date(replay.dateText); date.setUTCHours(Number(replay.hours)); date.setUTCMinutes(Number(replay.minutes)); date.setUTCSeconds(Number(replay.seconds)); let ts = new Date(replay.ts.getTime()); // diff less 10 seconds if (Math.abs(date.getTime() - ts.getTime()) < 10000) { return; } //console.log(replay.minutes.toString() + ' ' + ts.toString() + ' ' + (date.getTime() - ts.getTime()).toString()); //console.trace(); console.log('jump: ' + date.toUTCString()); replayClear(); loadReplay(date); } function replaySetTimeHint(arg) { replayJumpEnabled = false; let dateString; let timeString; dateString = zDateString(replay.ts); timeString = zuluTime(replay.ts) + NBSP + 'Z'; jQuery("#replayDateHint").html("Date: " + dateString); jQuery("#replayTimeHint").html("Time: " + timeString); 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; 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 (zoomLvl > 10) { ext = currentExtent(1.6); } else if (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 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(250, 1 / slideBase), slide: function(event, ui) { replay.speed = Math.pow(ui.value, slideBase).toFixed(1); jQuery('#replaySpeedHint').text('Speed: ' + replay.speed + 'x'); }, change: function(event, ui) { replayStep(); }, }); jQuery('#replaySpeedHint').text('Speed: ' + replay.speed + 'x'); jQuery("#selected_showTrace_hide").hide(); } }; function timeoutFetch() { console.log('timeoutFetch'); fetchData(); if (timers.timeoutFetch) { clearTimeout(timers.checkMove); } timers.timeoutFetch = setTimeout(timeoutFetch, Math.max(RefreshInterval, 10000)); if (now - lastReap > 120) { reaper(); } } function handleVisibilityChange() { const prevHidden = tabHidden; if (document[hideName]) tabHidden = true; else tabHidden = false; if (tabHidden && !prevHidden) { clearIntervalTimers(); if (!globeIndex) { timeoutFetch(); } replay_was_active = replay.playing; if (replay.playing) { playReplay(false); } } // tab is no longer hidden if (!tabHidden && prevHidden) { if (loadFinished) { jQuery("#timers_paused").css('display','none'); } globeRateUpdate().done(noLongerHidden); } } function noLongerHidden() { clearIntervalTimers(); setIntervalTimers(); active(); refresh(); fetchData(); if (replay_was_active) { playReplay(true); } if (showTrace) return; if (heatmap) return; if (!globeIndex) 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 (!closest) closest = plane; if (plane.position == null || !plane.visible) 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 += `\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); } // Create a "hidden" input let shareLinkInput = document.createElement("input"); // Append it to the body document.body.appendChild(shareLinkInput); function copyShareLink() { // Assign shareLinkInput the value we want to copy shareLinkInput.setAttribute("value", shareLink); // Highlight its content shareLinkInput.select(); // Copy the highlighted text document.execCommand("copy"); // deselect input field shareLinkInput.blur(); copyLinkTime = new Date().getTime(); copiedIcao = SelectedPlane.icao; setSelectedIcao(); } let copyLinkTime = 0; let copiedIcao = null; function setSelectedIcao() { const selected = SelectedPlane; if (selected.icao == selIcao && copiedIcao == null) { return; } selIcao = selected.icao; let hex_html = "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')) { layerExtraDim = -0.30; } else if (MapType_tar1090.startsWith('carto_raster')) { layerExtraDim = -0.15; layerExtraContrast = 0.6; } else if (MapType_tar1090.startsWith('carto_light')) { layerExtraDim = -0.05; layerExtraContrast = 0.2; } else { layerExtraDim = 0; layerExtraContrast = 0; } } function 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 (adsbexchange && 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 globeRateUpdate() { if (adsbexchange) { dynGlobeRate = true; const cookieExp = getCookie('adsbx_sid').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();