"use strict"; function PlaneObject(icao) { icao = `${icao}`; g.planes[icao] = this; g.planesOrdered.push(this); // Info about the plane this.icao = icao; const icaorange = findICAORange(icao); this.country = icaorange.country; this.country_code = icaorange.country_code; this.numHex = parseInt(icao.replace('~', '1'), 16); this.fakeHex = this.numHex > 16777215; // non-icao hex // most properties are set via this function so they can be reset easily this.setNull(); // Track history as a series of line segments this.elastic_feature = null; this.track_linesegs = []; this.history_size = 0; this.trace = []; // save last 30 seconds of positions this.lastTraceTs = 0; this.routeString = null; // Display info this.visible = false; this.marker = null; this.markerStyle = null; this.markerIcon = null; this.markerStyleKey = null; this.markerSvgKey = null; this.baseScale = 1; // start from a computed registration, let the DB override it // if it has something else. this.registration = registration_from_hexid(this.icao); this.icaoType = null; this.typeDescription = null; this.typeLong = null; this.wtc = null; this.dbinfoLoaded = false; // request metadata this.checkForDB(); // military icao ranges this.military = this.milRange(); } PlaneObject.prototype.setNull = function() { this.flight = null; this.flightTs = 0; this.name = 'no callsign'; this.squawk = null; this.category = null; this.dataSource = "modeS"; // Basic location information this.altitude = null; this.alt_baro = null; this.alt_geom = null; this.altitudeTime = 0; this.bad_alt = null; this.bad_altTime = null; this.alt_reliable = 0; this.speed = null; this.gs = null; this.ias = null; this.tas = null; this.track = null; this.track_rate = null; this.mag_heading = null; this.true_heading = null; this.mach = null; this.roll = null; this.nav_altitude = null; this.nav_heading = null; this.nav_modes = null; this.nav_qnh = null; this.rc = null; this.rotation = 0; this.rotationCache = 999; this.nac_p = null; this.nac_v = null; this.nic_baro = null; this.sil_type = null; this.sil = null; this.baro_rate = null; this.geom_rate = null; this.vert_rate = null; this.wd = null; this.ws = null; this.oat = null; this.tat = null; this.version = null; this.position = null; this.sitedist = null; this.too_fast = 0; // Track (direction) at the time we last appended to the track history this.tail_track = null; this.tail_true = null; // Timestamp of the most recent point appended to the track history this.tail_update = null; this.prev_position = null; this.prev_time = null; this.prev_track = null; // When was this last updated (receiver timestamp) this.seen = NaN; this.last_message_time = NaN; this.seen_pos = NaN; this.position_time = NaN; this.last_info_server = 0; this.last = 0; // last json this plane was included in // Data packet numbers this.messages = NaN; this.rssi = null; this.msgs1090 = 0; this.msgs978 = 0; this.messageRate = 0; this.messageRateOld = 0; }; function planeCloneState(target, source) { target.flight = source.flight; target.flightTs = source.flightTs; target.name = source.name; target.squawk = source.squawk; target.category = source.category; target.dataSource = source.dataSource; target.altitude = source.altitude; target.alt_baro = source.alt_baro; target.alt_geom = source.alt_geom; target.altitudeTime = source.altitudeTime; target.bad_alt = source.bad_alt; target.bad_altTime = source.bad_altTime; target.alt_reliable = source.alt_reliable; target.speed = source.speed; target.gs = source.gs; target.ias = source.ias; target.tas = source.tas; target.track = source.track; target.track_rate = source.track_rate; target.mag_heading = source.mag_heading; target.true_heading = source.true_heading; target.mach = source.mach; target.roll = source.roll; target.nav_altitude = source.nav_altitude; target.nav_heading = source.nav_heading; target.nav_modes = source.nav_modes; target.nav_qnh = source.nav_qnh; target.rc = source.rc; target.rotation = source.rotation; target.rotationCache = source.rotationCache; target.nac_p = source.nac_p; target.nac_v = source.nac_v; target.nic_baro = source.nic_baro; target.sil_type = source.sil_type; target.sil = source.sil; target.baro_rate = source.baro_rate; target.geom_rate = source.geom_rate; target.vert_rate = source.vert_rate; target.wd = source.wd; target.ws = source.ws; target.oat = source.oat; target.tat = source.tat; target.version = source.version; target.position = source.position; target.sitedist = source.sitedist; target.too_fast = source.too_fast; target.seen = source.seen; target.last_message_time = source.last_message_time; target.seen_pos = source.seen_pos; target.position_time = source.position_time; target.last = source.last; target.messages = source.messages; target.rssi = source.rssi; target.msgs1090 = source.msgs1090; target.msgs978 = source.msgs978; target.messageRate = source.messageRate; target.messageRateOld = source.messageRateOld; }; PlaneObject.prototype.checkLayers = function() { if (!this.trail_features) this.createFeatures(); if ((showTrace || trackLabels) && !this.trail_labels) this.createLabels(); }; PlaneObject.prototype.createFeatures = function() { this.trail_features = new ol.source.Vector(); this.layer = new ol.layer.Vector({ name: `${this.icao}`, isTrail: true, source: this.trail_features, declutter: false, zIndex: 150, renderOrder: null, }); trailGroup.push(this.layer); }; PlaneObject.prototype.createLabels = function() { this.trail_labels = new ol.source.Vector(); this.layer_labels = new ol.layer.Vector({ name: `${this.icao}_labels`, isTrail: true, source: this.trail_labels, declutter: true, zIndex: 151, }); trailGroup.push(this.layer_labels); }; PlaneObject.prototype.logSel = function(loggable) { if (debugTracks && this.selected && !SelectedAllPlanes && !globeIndex) console.log(loggable); return; }; PlaneObject.prototype.isFiltered = function() { if (this.selected) return false; if (noRegOnly && ( (this.registration || this.icao.startsWith('~')) || (this.category && this.category.startsWith('C')) || (this.squawk == '7777') || (this.icaoType == 'TWR') || (this.icaoType == 'GND') || (this.altitude == 'ground' && (this.addrtype == 'adsb_icao_nt' || this.addrtype == 'tisb_other')) )) { return true; } for (const filter of filters_active) { if (!this[filter.field] || !this[filter.field].toUpperCase().match(filter.PATTERN)) { //this[filter.field] && console.log(this[filter.field].toUpperCase() + ' ' + filter.PATTERN); return true; } } if (g.icao_nt_only && this.addrtype != 'adsb_icao_nt') { return true; } if (onlySelected && !this.selected) { return true; } if (onlyMilitary && !this.military) { return true; } if (nogpsOnly && !this.nogps) { return true; } if (!filterTracks && altFiltered(this.altitude)) return true; if (PlaneFilter.sources && PlaneFilter.sources.length > 0 && !PlaneFilter.sources.includes(this.dataSource)) { return true; } const flags = PlaneFilter.flagFilter; if (flags && flags.length > 0) { let found = false; for (const flag of flags) { if (this[flag]) { found = true; } } if (!found) { return true; } } // filter out ground vehicles if (PlaneFilter.groundVehicles == 'filtered') { if (typeof this.category === 'string' && this.category.startsWith('C')) return true; if (this.altitude == 'ground' && (this.addrtype == 'adsb_icao_nt' || this.addrtype == 'tisb_other')) return true; if (this.squawk == 7777) return true; } // filter out blocked MLAT flights if (PlaneFilter.blockedMLAT == 'filtered') { if (typeof this.icao === 'string' && this.icao.startsWith('~')) return true; } if (this.sitedist && this.sitedist > filterMaxRange) return true; return false; }; function altFiltered(altitude) { if (PlaneFilter.minAltitude == null || PlaneFilter.maxAltitude == null) return false; if (altitude == null) { return true; } const planeAltitude = altitude === "ground" ? 0 : altitude; if (planeAltitude < PlaneFilter.minAltitude || planeAltitude > PlaneFilter.maxAltitude) { return true; } return false; } PlaneObject.prototype.updateTail = function() { this.tail_update = this.prev_time; this.tail_track = this.prev_track; this.tail_rot = this.prev_rot; this.tail_true = this.prev_true; this.tail_position = this.prev_position; return this.updateTrackPrev(); }; PlaneObject.prototype.updateTrackPrev = function() { this.prev_position = this.position; this.prev_time = this.position_time; this.prev_track = this.track; this.prev_rot = this.rotation; this.prev_true = this.true_heading; this.prev_alt = this.altitude; this.prev_alt_rounded = this.alt_rounded; this.prev_alt_geom = this.alt_geom; this.prev_speed = this.speed; this.prev_rId = this.rId; this.prev_dataSource = this.dataSource; return true; }; // Appends data to the running track so we can get a visual tail on the plane // Only useful for a long running browser session. PlaneObject.prototype.updateTrack = function(now, last, serverTrack, stale) { if (this.position == null) return false; if (this.prev_position && this.position[0] == this.prev_position[0] && this.position[1] == this.prev_position[1] && !serverTrack) return false; if (this.bad_position && this.position[0] == this.bad_position[0] && this.position[1] == this.bad_position[1]) return false; if (this.position[0] > 180 || this.position[0] < -180 || this.position[1] > 90 || this.position[1] < -90) { console.log("Ignoring Impossible Position for " + this.icao + ": " + this.position); return false; } let projHere = ol.proj.fromLonLat(this.position); let on_ground = (this.altitude === "ground"); let is_leg = false; if (this.leg_ts == now) is_leg = 'end'; if (this.leg_ts == last) is_leg = 'start'; if (this.track_linesegs.length == 0) { // Brand new track //console.log(this.icao + " new track"); if (this.leg_ts == now) is_leg = 'start'; let newseg = { fixed: new ol.geom.LineString([projHere]), feature: null, estimated: false, ground: (this.altitude == "ground"), altitude: this.alt_rounded, alt_real: this.altitude, alt_geom: this.alt_geom, position: this.position, speed: this.speed, ts: now, track: this.rotation, leg: is_leg, rId: this.rId, dataSource: this.dataSource, }; this.track_linesegs.push(newseg); this.history_size ++; this.updateTrackPrev(); return this.updateTail(); } let lastseg = this.track_linesegs[this.track_linesegs.length - 1]; if (!this.prev_position) { return this.updateTail(); } let projPrev = ol.proj.fromLonLat(this.prev_position); if (!this.tail_position) { lastseg.fixed.appendCoordinate(projPrev); return this.updateTail(); } let distance = ol.sphere.getDistance(this.position, this.prev_position); let elapsed = this.position_time - this.prev_time; let derivedMach = 0.01; let filterSpeed = 10000; const pFilter = !serverTrack && (positionFilter == true || (positionFilter == 'onlyMLAT' && this.dataSource == "mlat")); if (pFilter) { derivedMach = (distance/(this.position_time - this.prev_time + 0.4))/343; filterSpeed = on_ground ? positionFilterSpeed/10 : positionFilterSpeed; filterSpeed = (this.speed != null && this.prev_speed != null) ? (positionFilterGsFactor*(Math.max(this.speed, this.prev_speed)+10+(this.dataSource == "mlat")*100)/666) : filterSpeed; } // ignore the position if the object moves faster than positionFilterSpeed (default Mach 3.5) // or faster than twice the transmitted groundspeed if (pFilter && derivedMach > filterSpeed && this.too_fast < 1) { this.bad_position = this.position; this.too_fast++; if (debugPosFilter) { console.log(this.icao + " / " + this.name + " ("+ this.dataSource + "): Implausible position filtered: " + this.bad_position[0] + ", " + this.bad_position[1] + " (kt/Mach " + (derivedMach*666).toFixed(0) + " > " + (filterSpeed*666).toFixed(0) + " / " + derivedMach.toFixed(2) + " > " + filterSpeed.toFixed(2) + ") (" + (this.position_time - this.prev_time + 0.2).toFixed(1) + "s)"); } this.position = this.prev_position; this.position_time = this.prev_time; if (debugPosFilter) { this.drawRedDot(this.bad_position); jumpTo = this.icao; } return false; } else { this.too_fast = Math.max(-5, this.too_fast-0.8); } if (this.request_rotation_from_track && this.prev_position) { this.rotation = bearingFromLonLat(this.prev_position, this.position); this.request_rotation_from_track = false; } // special case crossing the 180 -180 longitude line by just starting a new track if ((this.position[0] < -90 && this.prev_position[0] > 90) || (this.position[0] > 90 && this.prev_position[0] < -90) ) { lastseg.fixed.appendCoordinate(projPrev); this.cross180(on_ground, is_leg); this.history_size += 258; return this.updateTail(); } // Determine if track data are intermittent/stale // Time difference between two position updates should not be much // greater than the difference between data inputs let time_difference = (this.position_time - this.prev_time) - 2; if (g.refreshHistory || !loadFinished || serverTrack) time_difference = (this.position_time - this.prev_time) - Math.min(60, now - last); //let stale_timeout = lastseg.estimated ? 5 : 10; let stale_timeout = 15; // MLAT data are given some more leeway if (this.dataSource == "mlat") stale_timeout = 15; // On the ground you can't go that quick if (on_ground) stale_timeout = 30; if (pTracks && !serverTrack) { stale = false; stale_timeout = 120; if (this.dataSource == "adsc") stale_timeout = jaeroTimeout; } const modeS = (this.prev_dataSource == 'modeS'); if (replay) stale_timeout = 2 * replay.ival + 1; // Also check if the position was already stale when it was exported by dump1090 // Makes stale check more accurate for example for 30s spaced history points let estimated = (time_difference > stale_timeout) || ((now - this.position_time) > stale_timeout) || stale; if (estimated) { //console.trace(); //console.log('estimated ' + new Date(1000 * this.position_time) + ' ' + this.position); } /* let track_change = this.track != null ? Math.abs(this.tail_track - this.track) : NaN; track_change = track_change < 180 ? track_change : Math.abs(track_change - 360); let true_change = this.trueheading != null ? Math.abs(this.tail_true - this.true_heading) : NaN; true_change = true_change < 180 ? true_change : Math.abs(true_change - 360); if (!isNaN(true_change)) { track_change = isNaN(track_change) ? true_change : Math.max(track_change, true_change); } */ let track_change = Math.abs(this.tail_rot - this.rotation); let alt_change = Math.abs(this.alt_rounded - lastseg.altitude); let since_update = this.prev_time - this.tail_update; let distance_traveled = ol.sphere.getDistance(this.tail_position, this.prev_position); if (pTracks && since_update < pTracksInterval && !serverTrack) { return this.updateTrackPrev(); } if ( this.prev_alt_rounded !== lastseg.altitude || modeS || this.prev_time > lastseg.ts + 300 || (!pTracks && this.prev_time > lastseg.ts + 15) || estimated != lastseg.estimated || estimated || tempTrails || debugAll || serverTrack || !globeIndex && ( track_change > 5 || Math.abs(this.prev_speed - lastseg.speed) > 10 || Math.abs(this.prev_alt - lastseg.alt_real) > 200 ) //lastseg.ground != on_ground //|| (!on_ground && isNaN(alt_change)) //|| (alt_change > 700) //|| (alt_change > 375 && this.alt_rounded < 9000) //|| (alt_change > 150 && this.alt_rounded < 5500) ) { // Create a new segment as the ground state or the altitude changed. // The new state is only drawn after the state has changed // and we then get a new position. if (verboseUpdateTrack) { this.logSel("sec_elapsed: " + since_update.toFixed(1) + " alt_change: "+ alt_change.toFixed(0) + " derived_speed(kt/Mach): " + (distance_traveled/since_update*1.94384).toFixed(0) + " / " + (distance_traveled/since_update/343).toFixed(1) + " dist:" + distance_traveled.toFixed(0)); } let segments = [[projPrev]]; if ((since_update > 3600 && distance_traveled / since_update * 3.6 < 100) || modeS) { // don't draw a line if a long time has elapsed but no great distance was traveled } else { lastseg.fixed.appendCoordinate(projPrev); } let estimatedFill = false; // draw great circle path for long distances if (distance > 30000 && !(elapsed > 3600 && distance / elapsed * 3.6 < 100) && !modeS // don't draw a line if a long time has elapsed but no great distance was traveled ) { estimatedFill = true; if (!(pTracks && !serverTrack)) { estimated = true; } let nPoints = distance / 19000; let greyskull = Math.ceil(Math.log(nPoints) / Math.log(2)); //console.log(Math.round(nPoints) + ' ' + greyskull); let points = makeCircle([this.prev_position, this.position], greyskull); segments = [[]]; let seg_index = 0; let last_lon = this.prev_position[0]; for (let i in points) { let point = points[i]; let lon = point[0]; if (Math.abs(last_lon - lon) > 270) { //console.log(i + ' ' + point); segments.push([]); seg_index++; } let segment = segments[seg_index]; segment.push(ol.proj.fromLonLat(point)); last_lon = lon; } } for (let i in segments) { let points = segments[i]; this.track_linesegs.push({ fixed: new ol.geom.LineString(points), feature: null, estimated: estimated, estimatedFill: estimatedFill, ground: (this.prev_alt == "ground"), altitude: this.prev_alt_rounded, alt_real: this.prev_alt, alt_geom: this.prev_alt_geom, position: this.prev_position, speed: this.prev_speed, ts: this.prev_time, track: this.prev_rot, leg: is_leg, rId: this.prev_rId, dataSource: this.prev_dataSource, noLabel: (i > 0), }); } this.history_size += 2; return this.updateTail(); } if (modeS) { return this.updateTrackPrev(); } // Add current position to the existing track. // We only retain some points depending on time elapsed and track change let turn_density = 6.5; if (pTracks && pTracksInterval > 5) turn_density = 3; if ( since_update > 86 + !!pTracks * 90 || (!on_ground && since_update > (100/turn_density)/track_change) || (!on_ground && isNaN(track_change) && since_update > 8 + !!pTracks * 22) || (on_ground && since_update > (120/turn_density)/track_change && distance_traveled > 20) || (on_ground && distance_traveled > 50 && since_update > 5) || debugAll ) { lastseg.fixed.appendCoordinate(projPrev); this.history_size ++; this.logSel("sec_elapsed: " + since_update.toFixed(1) + " " + (on_ground ? "ground" : "air") + " dist:" + distance_traveled.toFixed(0) + " track_change: "+ track_change.toFixed(1) + " derived_speed(kt/Mach): " + (distance_traveled/since_update*1.94384).toFixed(0) + " / " + (distance_traveled/since_update/343).toFixed(1)); return this.updateTail(); } return this.updateTrackPrev(); }; PlaneObject.prototype.getDataSourceNumber = function() { if (this.dataSource == "modeS") return 5; if (this.dataSource == "adsc") return 6; if (this.dataSource == "mlat") return 3; if (this.dataSource == "uat" || (this.addrtype && this.addrtype.substring(0,4) == "adsr")) return 2; // UAT if (this.dataSource == "tisb") return 4; // TIS-B if (this.dataSource == "adsb") return 1; if (this.dataSource == "other" || this.dataSource == "ais") return 7; return 8; }; PlaneObject.prototype.getDataSource = function() { return this.dataSource; }; PlaneObject.prototype.getMarkerColor = function(options) { options |= {} if (monochromeMarkers) { return hexToHSL(monochromeMarkers); } let alt = options.noRound ? this.altitude : this.alt_rounded; if (this.category == 'C3' || this.icaoType == 'TWR' || (this.icaoType == null && this.squawk == 7777)) alt = 'ground'; let h, s, l; let colorArr = altitudeColor(alt); h = colorArr[0]; s = colorArr[1]; l = colorArr[2]; // If we have not seen a recent position update, change color if ((this.dataSource == 'adsc' && this.seen_pos > 20 * 60) || (!globeIndex && this.dataSource != 'adsc' && this.seen_pos > 15)) { h += ColorByAlt.stale.h; s += ColorByAlt.stale.s; l += ColorByAlt.stale.l; } if (alt == "ground") { l += 15; } // If this marker is selected, change color if (this.selected && !SelectedAllPlanes && !onlySelected){ h += ColorByAlt.selected.h; s += ColorByAlt.selected.s; l += ColorByAlt.selected.l; } // If this marker is a mlat position, change color if (this.dataSource == "mlat") { h += ColorByAlt.mlat.h; s += ColorByAlt.mlat.s; l += ColorByAlt.mlat.l; } if (atcStyle && (this.squawk == '7700' || this.squawk == '7600' || this.squawk == '7500')) { h = 0; s = 100; l = 40; } if (h < 0) { h = (h % 360) + 360; } else if (h >= 360) { h = h % 360; } if (s < 0) s = 0; else if (s > 95) s = 95; if (l < 0) l = 0; else if (l > 95) l = 95; return [h, s, l]; }; function altitudeColor(altitude) { altitude = adjust_baro_alt(altitude); let h, s, l; if (altitude == null) { h = ColorByAlt.unknown.h; s = ColorByAlt.unknown.s; l = ColorByAlt.unknown.l; } else if (altitude === "ground") { h = ColorByAlt.ground.h; s = ColorByAlt.ground.s; l = ColorByAlt.ground.l; } else { const altRound = (altitude < 8000) ? 50 : ((webgl && !pTracks && !SelectedAllPlanes) ? 200 : 500); // round altitude to limit the number of colors used altitude = altRound * Math.round(altitude / altRound); s = ColorByAlt.air.s; // find the pair of points the current altitude lies between, // and interpolate the hue between those points let hpoints = ColorByAlt.air.h; h = hpoints[0].val; for (let i = hpoints.length-1; i >= 0; --i) { if (altitude > hpoints[i].alt) { if (i == hpoints.length-1) { h = hpoints[i].val; } else { h = hpoints[i].val + (hpoints[i+1].val - hpoints[i].val) * (altitude - hpoints[i].alt) / (hpoints[i+1].alt - hpoints[i].alt) } break; } } let lpoints = ColorByAlt.air.l; lpoints = lpoints.length ? lpoints : [{h:0, val:lpoints}]; l = lpoints[0].val; for (let i = lpoints.length-1; i >= 0; --i) { if (h > lpoints[i].h) { if (i == lpoints.length-1) { l = lpoints[i].val; } else { l = lpoints[i].val + (lpoints[i+1].val - lpoints[i].val) * (h - lpoints[i].h) / (lpoints[i+1].h - lpoints[i].h) } break; } } } if (darkerColors) { l *= 0.8; s *= 0.7; } if (h < 0) { h = (h % 360) + 360; } else if (h >= 360) { h = h % 360; } if (s < 0) s = 0; else if (s > 95) s = 95; if (l < 0) l = 0; else if (l > 95) l = 95; return [h, s, l]; } PlaneObject.prototype.setMarkerRgb = function() { let hsl = this.getMarkerColor({noRound: true}); let rgb = hslToRgb(hsl, 'array'); if (this.shape && this.shape.svg) rgb = [255, 255, 255]; this.glMarker.set('r', rgb[0]); this.glMarker.set('g', rgb[1]); this.glMarker.set('b', rgb[2]); }; PlaneObject.prototype.updateIcon = function() { let fillColor = hslToRgb(this.getMarkerColor()); let svgKey = fillColor + '!' + this.shape.name + '!' + this.strokeWidth; let labelText = null; if ( g.enableLabels && (!multiSelect || (multiSelect && this.selected)) && ( (g.zoomLvl >= labelZoom && this.altitude != "ground" && this.dataSource != "ais") || (g.zoomLvl >= labelZoomGround - 2 && this.speed > 5 && !this.fakeHex) || (g.zoomLvl >= labelZoomGround + 0 && !this.fakeHex) || (g.zoomLvl >= labelZoomGround + 1) || this.selected ) ) { let callsign = ""; if (this.flight && this.flight.trim() && !(this.dataSource == "ais" && !g.extendedLabels)) callsign = this.flight.trim(); else if (this.registration) callsign = 'reg: ' + this.registration; else callsign = 'hex: ' + this.icao; if ((useRouteAPI || this.dataSource == "ais") && this.routeString) { if (0 && g.extendedLabels) { callsign += ' - ' + this.routeString; } else { callsign += '\n' + this.routeString; } } const unknown = NBSP+NBSP+"?"+NBSP+NBSP; let alt; if (labelsGeom) { alt = adjust_geom_alt(this.alt_geom, this.position); } else { alt = adjust_baro_alt(this.altitude); } let altString = (alt == null) ? unknown : format_altitude_brief(alt, this.vert_rate, DisplayUnits, showLabelUnits); let speedString = (this.speed == null) ? (NBSP+'?'+NBSP) : format_speed_brief(this.speed, DisplayUnits, showLabelUnits).padStart(3, NBSP); labelText = ""; if (atcStyle) { labelText += callsign + '\n'; labelText += altString + '\n'; labelText += 'x' + this.squawk; if (this.squawk == '7700' || this.squawk == '7600' || this.squawk == '7500') { if (this.squawk == '7700') { labelText += '\nEMERGENCY'; } else if (this.squawk == '7600') { labelText += '\nNORDO'; } else if (this.squawk == '7500') { labelText += '\nHIJACK'; } } } else if (g.extendedLabels == 3) { if (!windLabelsSlim) { labelText += 'Wind' + NBSP; } if (this.wd != null) { if (showLabelUnits) { labelText += format_track_arrow((this.wd + 180 % 360)) + NBSP + this.wd + '°' + NBSP; labelText += format_speed_long(this.ws, DisplayUnits); } else { labelText += format_track_arrow((this.wd + 180 % 360)) + NBSP + this.wd + NBSP; labelText += format_speed_brief(this.ws, DisplayUnits); } } else { labelText += 'n/a'; } if (windLabelsSlim) { labelText += '\n' + altString; } else { if ((!this.onGround || (this.speed && this.speed > 18) || (this.selected && !SelectedAllPlanes))) { labelText += '\n' + speedString + NBSP + NNBSP + altString.padStart(6, NBSP); } labelText += '\n' + callsign; } if (windLabelsSlim && this.wd == null) { labelText = ''; } } else if (g.extendedLabels == 2) { labelText += (this.registration ? this.registration : unknown) + NBSP + (this.icaoType ? this.icaoType : unknown) + '\n'; } if (g.extendedLabels == 1 || g.extendedLabels == 2) { if ((!this.onGround || (this.speed && this.speed > 18) || (this.selected && !SelectedAllPlanes))) { labelText += speedString + NBSP + NNBSP + altString.padStart(6, NBSP) + '\n'; } } if (g.extendedLabels < 3 && !atcStyle) { labelText += callsign; } } if (!webgl && (this.markerStyle == null || this.markerIcon == null || (this.markerSvgKey != svgKey))) { //console.log(this.icao + " new icon and style " + this.markerSvgKey + " -> " + svgKey); if (iconCache[svgKey] == undefined) { let svgURI = svgShapeToURI(this.shape, fillColor, OutlineADSBColor, this.strokeWidth); addToIconCache.push([svgKey, null, svgURI]); if (true || TrackedAircraftPositions < 200) { this.markerIcon = new ol.style.Icon({ scale: this.scale, imgSize: [this.shape.w, this.shape.h], src: svgURI, rotation: (this.shape.noRotate ? 0 : this.rotation * Math.PI / 180.0), rotateWithView: (this.shape.noRotate ? false : true), }); this.scaleCache = this.scale; } else { svgKey = this.markerSvgKey; } } else { this.markerIcon = new ol.style.Icon({ scale: this.scale, imgSize: [this.shape.w, this.shape.h], img: iconCache[svgKey], rotation: (this.shape.noRotate ? 0 : this.rotation * Math.PI / 180.0), rotateWithView: (this.shape.noRotate ? false : true), }); this.scaleCache = this.scale; } this.markerSvgKey = svgKey; //iconCache[svgKey] = undefined; // disable caching for testing } if (!this.markerIcon && !webgl) return; let styleKey = (webgl ? '' : svgKey) + '!' + labelText + '!' + this.scale; if (this.styleKey != styleKey || !this.marker.getStyle()) { this.styleKey = styleKey; let style; if (labelText) { style = { image: this.markerIcon, text: new ol.style.Text({ text: labelText, fill: labelFill, backgroundFill: bgFill, stroke: labelStrokeNarrow, textAlign: 'left', textBaseline: labels_top ? 'bottom' : 'top', font: labelFont, offsetX: (this.shape.w *0.5*0.74*this.scale), offsetY: labels_top ? (this.shape.w *-0.3*0.74*this.scale) : (this.shape.w *0.5*0.74*this.scale), padding: [1, 0, -1, 2], }), zIndex: this.zIndex, }; } else { style = { image: this.markerIcon, zIndex: this.zIndex, }; } if (webgl) delete style.image; this.markerStyle = new ol.style.Style(style); this.marker.setStyle(this.markerStyle); } if (webgl) return; /* if (this.opacityCache != opacity) { this.opacityCache = opacity; this.markerIcon.setOpacity(opacity); } */ const iconRotation = this.shape.noRotate ? 0 : this.rotation; if (this.rotationCache != iconRotation && Math.abs(this.rotationCache - iconRotation) > 0.35) { this.rotationCache = iconRotation; this.markerIcon.setRotation(iconRotation * Math.PI / 180.0); } if (this.scaleCache != this.scale) { this.scaleCache = this.scale; this.markerIcon.setScale(this.scale); } return; }; PlaneObject.prototype.processTrace = function() { const old_last_info_server = this.last_info_server; if (this.fullTrace && !this.fullTrace.trace) { this.fullTrace = null; } if (this.recentTrace && !this.recentTrace.trace) { this.recentTrace = null; } if (replay && !this.fullTrace) return; if (!showTrace && !this.fullTrace && !this.recentTrace) { return; } if (!now) now = new Date().getTime()/1000; if (showTrace || replay) this.setNull(); let legStart = traceOpts.legStart; let legEnd = traceOpts.legEnd; this.checkLayers(); let trace = null; let _now, _last = 0; this.history_size = 0; let points_in_trace = 0; let pointsRecent = 0; if (!traceOpts.showTime) { this.resetTrail(); } let firstPos = null; let tempPlane = {}; planeCloneState(tempPlane, this); this.position = null; if (this.fullTrace && this.recentTrace && this.fullTrace.length > 0 && this.recentTrace.length > 0) { let t1 = this.fullTrace.trace; let t2 = this.recentTrace.trace; let end1 = t1[t1.length-1][0]; let start2 = t2[0][0]; if (end1 < start2) console.log("Insufficient recent trace overlap!"); } if (this.recentTrace && !this.fullTrace) { trace = this.recentTrace.trace; } else if (this.fullTrace) { trace = this.fullTrace.trace; if (this.recentTrace) { const recent = this.recentTrace.trace; for (let i = 0; i < recent.length; i++) { const entry = recent[i]; if (trace.length == 0 || entry[0] > trace[trace.length - 1][0]) { //console.log("pushing " + entry[0]); trace.push(entry); } } } } if (trace && trace.length > 0) { let start = 0; let end = trace.length; _last = trace[0][0] - 1; if (legStart != null) start = legStart; if (legEnd != null) end = legEnd; if (traceOpts.startStamp != null) { let found = 0; for (let i = start; i < end; i++) { const timestamp = trace[i][0]; if (timestamp >= traceOpts.startStamp) { start = i; found = 1 break; } } if (!found) { start = end = 0; } } if (traceOpts.endStamp != null) { let found = 0; for (let i = end - 1; i >= start; i--) { const timestamp = trace[i][0]; if (timestamp <= traceOpts.endStamp) { end = i + 1; found = 1 break; } } if (!found) { start = end = 0; } } if (lastLeg && !showTrace) { for (let i = end - 1; i >= start; i--) { if (trace[i][6] & 2) { start = i; break; } } } //console.log('trace points from/to: ' + trace[start][0] + ' ' + trace[end-1][0]); let lastPosition; for (let i = start; i < end; i++) { const state = trace[i]; const timestamp = state[0]; let stale = state[6] & 1; const leg_marker = state[6] & 2; if (1000 * timestamp > new Date().getTime()) { console.log('in the future ' + new Date(timestamp * 1000) + ' ' + state.join(',')); } // no going backwards in time if (timestamp < _now) { console.log('backwards trace wat? ' + timestamp + ' ' + state.join(',')); continue; } _now = timestamp; if (traceOpts.showTime && timestamp > traceOpts.showTime) { if (traceOpts.replaySpeed > 0) { clearTimeout(traceOpts.showTimeout); traceOpts.animateRealtime = (timestamp - traceOpts.showTime) * 1000; traceOpts.animateTime = traceOpts.animateRealtime / traceOpts.replaySpeed; let fps = webgl ? 28 : 1; traceOpts.animateSteps = Math.round(traceOpts.animateTime / (1000 / fps)); traceOpts.animateCounter = traceOpts.animateSteps; // will count down traceOpts.animateStepTime = traceOpts.animateRealtime / traceOpts.replaySpeed / traceOpts.animateSteps; if (traceOpts.animateSteps < 2) { traceOpts.animate = false; //console.log(`animateTime: ${traceOpts.animateTime}`); traceOpts.showTime = timestamp; traceOpts.showTimeout = setTimeout(gotoTime, traceOpts.animateTime); } else { traceOpts.showTimeEnd = timestamp; //console.timeEnd('step'); //console.time('step'); //console.log(traceOpts.animateTime); traceOpts.animate = true; traceOpts.animateFromLon = this.position[0]; traceOpts.animateFromLat = this.position[1]; traceOpts.animateToLon = state[2]; traceOpts.animateToLat = state[1]; traceOpts.animatePos = [traceOpts.animateFromLon, traceOpts.animateFromLat]; //console.log('from: ', fromProj); //console.log('to: ', toProj); traceOpts.showTimeout = setTimeout(gotoTime, traceOpts.animateStepTime); } } break; } if (now - timestamp < 3 * 60 * 60) { pointsRecent++; } points_in_trace++; this.updateTraceData(state, _now); if (firstPos == null) firstPos = this.position; if (leg_marker) { this.leg_ts = _now; if (debugTracks) { console.log('leg zulu: ' + zuluTime(new Date(this.leg_ts * 1000)) + ' epoch: ' + this.leg_ts); } } if (legStart != null && legStart > 0 && legStart == i) { this.leg_ts = _now; } if (legEnd != null && legEnd < trace.length && legEnd == i + 1) { this.leg_ts = _now; } if (_last - _now > 320) { stale = true; } if (!traceOpts.showTime) { this.updateTrack(_now, _last, true, stale); } else if (this.track == null && lastPosition && this.request_rotation_from_track) { this.rotation = bearingFromLonLat(lastPosition, this.position); this.request_rotation_from_track = false; } _last = _now; lastPosition = this.position; // go only 1 step beyond now for replay, end of replay.ival is obeyed via traceOpts.endStamp if (replay && timestamp >= now) { //console.log(timestamp - now); break; } } } for (let i = 0; i < this.trace.length; i++) { if (showTrace || replay) break; const state = this.trace[i]; // no going backwards in time if (state.now <= _now) { //console.log(new Date(1000 * state.now)); continue; } _now = state.now; this.position = state.position; this.position_time = _now; this.altitude = state.altitude; this.alt_rounded = state.alt_rounded; this.speed = state.speed; this.track = state.track; this.rotation = state.rotation; if (_last - _now > 30) { _last = _now - 1; } this.updateTrack(_now, _last); _last = _now; } //if ((this.position == null || tempPlane.position != null) && tempPlane.position_time > this.position_time && !showTrace && !replay) {} //console.log(tempPlane.position_time + ' ' + this.position_time); if (tempPlane.last_message_time > this.last_message_time && !showTrace && !replay) { planeCloneState(this, tempPlane); this.updateTrack(this.position_time, _last); } if (showTrace && !traceOpts.showTime) { if (this.track_linesegs.length > 0 && this.position) { const proj = ol.proj.fromLonLat(this.position); this.track_linesegs[this.track_linesegs.length - 1].fixed.appendCoordinate(proj); this.track_linesegs.push({ fixed: new ol.geom.LineString([proj]), feature: null, estimated: false, ground: (this.altitude == "ground"), altitude: this.alt_rounded, alt_real: this.altitude, alt_geom: this.alt_geom, position: this.position, speed: this.speed, ts: this.position_time, track: this.rotation, rId: this.rId, dataSource: this.dataSource, }); } now = new Date().getTime()/1000; } if (showTrace) { this.seen = 0; this.seen_pos = 0; } this.visible = true; if (traceOpts.follow) toggleFollow(true); if (traceOpts.showTime) { this.updateMarker(true); } else { this.updateFeatures(true); } let mapSize = OLMap.getSize(); let size = [Math.max(5, mapSize[0] - 280), mapSize[1]]; if (!traceOpts.showTime && (showTrace || showTraceExit) && !multiSelect && this.position && !(traceOpts.noFollow && traceOpts.noFollow + 3 > new Date().getTime() / 1000) && !inView(this.position, myExtent(OLMap.getView().calculateExtent(size))) && !inView(firstPos, myExtent(OLMap.getView().calculateExtent(size)))) { OLMap.getView().setCenter(ol.proj.fromLonLat(this.position)); } showTraceExit = false; this.checkForDB(this.recentTrace || this.fullTrace); this.dataChanged(); refreshHighlighted(); refreshSelected(); if (showTrace) { TAR.planeMan.refresh(); updateAddressBar(); } this.updateTick(); if (debugTracks) { console.log('3h: ' + pointsRecent.toString().padStart(4, ' ') + ' total: ' + points_in_trace); } if (old_last_info_server > this.last_info_server || !this.last_info_server) { this.last_info_server = old_last_info_server; } }; PlaneObject.prototype.updatePositionData = function(now, last, data, init) { if (this.position && SitePosition) { if (pTracks && this.sitedist) { this.sitedist = Math.max(ol.sphere.getDistance(SitePosition, this.position), this.sitedist); } else if (!init || pTracks) { this.sitedist = ol.sphere.getDistance(SitePosition, this.position); } } if (!globeIndex || this.selected || SelectedAllPlanes || replay) { let newPos = this.updateTrack(now, last); this.drawLine |= newPos; } if (globeIndex && !replay && this.position && this.position_time) { if (this.position_time > this.lastTraceTs + 0.1) { this.lastTraceTs = this.position_time; this.trace.push({ now: this.position_time, position: this.position, altitude: this.altitude, alt_rounded: this.alt_rounded, speed: this.speed, track: this.track, rotation: this.rotation, }); if (this.trace.length > 100) { this.trace = this.trace.slice(-80); } } } this.dataChanged(); } // Update our data PlaneObject.prototype.updateData = function(now, last, data, init) { // get location data first, return early if only those are needed. this.last_info_server = now; let isArray = Array.isArray(data); // [.hex, .alt_baro, .gs, .track, .lat, .lon, .seen_pos, "mlat"/"tisb"/.type , .flight, .messages] // 0 1 2 3 4 5 6 7 8 9 // this format is only valid for chunk loading the history const alt_baro = isArray? data[1] : data.alt_baro; let gs = isArray? data[2] : data.gs; const track = isArray? data[3] : data.track; const lat = isArray? data[4] : data.lat; const lon = isArray? data[5] : data.lon; let seen = isArray? data[6] : data.seen; let seen_pos = isArray? data[6] : data.seen_pos; seen = (seen == null) ? 5 : seen; seen_pos = (seen_pos == null && lat) ? 5 : seen_pos; let type = isArray? data[7] : data.type; if (!isArray && data.mlat != null && data.mlat.indexOf("lat") >= 0) type = 'mlat'; const mlat = (type == 'mlat'); const tisb = (type && type.substring(0,4) == "tisb"); const flight = isArray? data[8] : data.flight; this.last_message_time = now - seen; // remember last known position even if stale // do not show or process mlat positions when filtered out. if (lat != null && lon != null && !(noMLAT && mlat)) { this.position = [lon, lat]; this.position_time = now - seen_pos; } // remember last known altitude even if stale let newAlt = null; if (alt_baro != null) { newAlt = alt_baro; this.alt_baro = alt_baro; } else if (data.altitude != null) { newAlt = data.altitude; this.alt_baro = data.altitude; } else { this.alt_baro = null; if (data.alt_geom != null) { newAlt = data.alt_geom; } } // Filter anything greater than 12000 fpm if (newAlt == null || (newAlt == this.bad_alt && this.seen_pos > 5)) { // do nothing } else if ( !altitudeFilter || this.altitude == null || newAlt == "ground" || this.altitude == "ground" || (seen_pos != null && seen_pos < 2) ) { this.altitude = newAlt; this.altitudeTime = now; } else if ( this.alt_reliable > 0 && this.altBad(newAlt, this.altitude, this.altitudeTime, data) && (this.bad_alt == null || this.altBad(newAlt, this.bad_alt, this.bad_altTime, data)) ) { // filter this altitude! this.alt_reliable--; this.bad_alt = newAlt; this.bad_altTime = now; if (debugPosFilter) { console.log((now%1000).toFixed(0) + ': AltFilter: ' + this.icao + ' oldAlt: ' + this.altitude + ' newAlt: ' + newAlt + ' elapsed: ' + (now-this.altitudeTime).toFixed(0) ); jumpTo = this.icao; } } else { // good altitude this.altitude = newAlt; this.altitudeTime = now; this.alt_reliable = Math.min(this.alt_reliable + 1, 3); } this.updateAlt(); if (data.speed != null) { gs = data.speed; } // needed for track labels if (pTracks) { if (gs != null) { this.speed = Math.max(this.speed, gs); } this.gs = gs; } else { this.speed = gs; this.gs = gs; } this.track = track; if (track != null) { this.rotation = track; } else if (data.calc_track) { this.rotation = data.calc_track; } else { this.request_rotation_from_track = true; } this.setTypeFlagsReg(data); this.setFlight(flight); if (mlat && noMLAT) { this.dataSource = "modeS"; } else if (mlat) { this.dataSource = "mlat"; } else if (!displayUATasADSB && this.uat && !tisb) { this.dataSource = "uat"; } else if (tisb) { this.dataSource = "tisb"; } else if (type && type.substring(0,4) == "adsr") { this.dataSource = "adsr"; } else if ((lat != null && type == null) || (type && (type.substring(0,4) == "adsb"))) { this.dataSource = "adsb"; } else if (type == 'adsc') { this.dataSource = "adsc"; } else if (type == 'mode_s') { this.dataSource = "modeS"; } else if (type == 'other') { this.dataSource = "other"; } else if (type == 'unknown') { this.dataSource = "unknown"; } else if (type == 'ais') { this.dataSource = "ais"; } if (isArray) { this.messages = data[9]; this.updatePositionData(now, last, data, init); return; } // Update all of our data if (data.messages < this.messages && binCraft) this.messages -= 65536; const elapsed = now - this.last; if (elapsed > 0) { let messageRate = 0; if (!this.uat) { messageRate = (data.messages - this.msgs1090)/(now - this.last); this.msgs1090 = data.messages; } else { messageRate = (data.messages - this.msgs978)/(uat_now - uat_last); this.msgs978 = data.messages; } if (elapsed > 60) messageRate = 0; this.messageRate = (messageRate + this.messageRateOld)/2; this.messageRateOld = messageRate; } this.messages = data.messages; if (data.messageRate != null) { this.messageRate = data.messageRate; } if (data.rssi != null && data.rssi > -49.4) { this.rssi = data.rssi; } else { this.rssi = null; } if (data.baro_rate != null) this.baro_rate = data.baro_rate; else if (data.vert_rate != null) this.baro_rate = data.vert_rate; else this.baro_rate = null; // simple fields this.alt_geom = data.alt_geom; this.ias = data.ias; this.tas = data.tas; this.track_rate = data.track_rate; this.mag_heading = data.mag_heading; this.mach = data.mach; this.roll = data.roll; this.nav_altitude = data.nav_altitude; this.nav_heading = data.nav_heading; this.nav_modes = data.nav_modes; this.nac_p = data.nac_p; this.nac_v = data.nac_v; this.nic_baro = data.nic_baro; this.sil_type = data.sil_type; this.sil = data.sil; this.nav_qnh = data.nav_qnh; this.geom_rate = data.geom_rate; this.rc = data.rc; if (!replay || data.squawk != null) this.squawk = (data.squawk == null) ? null : `${data.squawk}`; this.wd = data.wd; this.ws = data.ws; this.oat = data.oat; this.tat = data.tat; this.receiverCount = data.receiverCount; // fields with more complex behaviour if (data.version != null) { this.version = data.version; } if (data.category != null) { this.category = `${data.category}`; } if (data.true_heading != null) this.true_heading = data.true_heading; else this.true_heading = null; if (type != null) this.addrtype = type; else this.addrtype = null; // Pick a selected altitude if (data.nav_altitude_fms != null) { this.nav_altitude = data.nav_altitude_fms; } else if (data.nav_altitude_mcp != null){ this.nav_altitude = data.nav_altitude_mcp; } else { this.nav_altitude = null; } // Pick vertical rate from either baro or geom rate if (data.baro_rate != null) { this.vert_rate = data.baro_rate; } else if (data.geom_rate != null ) { this.vert_rate = data.geom_rate; } else if (data.vert_rate != null) { // legacy from mut v 1.15 this.vert_rate = data.vert_rate; } else { this.vert_rate = null; } this.request_rotation_from_track = false; if (replay) { this.request_rotation_from_track = true; } else if (this.altitude == "ground") { if (this.true_heading != null) this.rotation = this.true_heading; else if (this.mag_heading != null) this.rotation = this.mag_heading; else if (data.calc_track) this.rotation = data.calc_track; } else if (this.track != null) { this.rotation = this.track; } else if (this.true_heading != null) { this.rotation = this.true_heading; } else if (this.mag_heading != null) { this.rotation = this.mag_heading; } else if (data.calc_track) { this.rotation = data.calc_track; } else { this.request_rotation_from_track = true; } if (data.nogps != null) { this.nogps = data.nogps; } this.rId = data.rId; if (!this.dbinfoLoaded) { this.checkForDB(data); } if (data.route) { this.routeString = data.route; } this.last = now; this.updatePositionData(now, last, data, init); return; }; PlaneObject.prototype.updateTick = function(redraw) { this.updateVisible(); this.updateFeatures(redraw); }; PlaneObject.prototype.updateVisible = function() { this.inView = inView(this.position, lastRenderExtent); this.visible = this.checkVisible() && !this.isFiltered(); }; PlaneObject.prototype.updateFeatures = function(redraw) { if (this.visible) { if (this.drawLine || redraw || this.lastVisible != this.visible) this.updateLines(); this.updateMarker(redraw); } if (!this.visible && this.lastVisible) { this.clearMarker(); this.clearLines(); } this.lastVisible = this.visible; }; PlaneObject.prototype.clearMarker = function() { this.markerDrawn = false; if (this.marker && this.marker.visible) { PlaneIconFeatures.removeFeature(this.marker); this.marker.visible = false; } delete this.marker; if (this.glMarker && this.glMarker.visible) { webglFeatures.removeFeature(this.glMarker); this.glMarker.visible = false; } delete this.glMarker; delete this.styleKey; delete this.olPoint; }; // Update our marker on the map PlaneObject.prototype.updateMarker = function(moved) { if (!this.visible || this.position == null || (pTracks && (SelectedAllPlanes || !this.selected))) { if (this.markerDrawn) this.clearMarker(); return; } this.markerDrawn = true; this.setProjection(); let eastbound = this.rotation < 180; let icaoType = this.icaoType; if (this.speed > 120) { if (icaoType == 'V22') icaoType = 'V22F'; if (icaoType == 'B609') icaoType = 'B609F'; } if (icaoType == null && this.squawk == 7777) icaoType = 'TWR'; let baseMarkerKey = this.category + "_" + this.typeDescription + "_" + this.wtc + "_" + icaoType + '_' + (this.altitude == "ground") + eastbound; if (!this.shape || this.baseMarkerKey != baseMarkerKey) { this.baseMarkerKey = baseMarkerKey; let baseMarker = null; try { baseMarker = getBaseMarker(this.category, icaoType, this.typeDescription, this.wtc, this.addrtype, this.altitude, eastbound); } catch (error) { console.error(error); console.log(baseMarkerKey); } if (!baseMarker) { baseMarker = ['pumpkin', 1]; } this.shape = shapes[baseMarker[0]]; this.baseScale = baseMarker[1] * 0.96; } this.scale = iconSize * this.baseScale; this.strokeWidth = outlineWidth * ((this.selected && !SelectedAllPlanes && !onlySelected) ? 0.85 : 0.7) / this.baseScale; if (!this.marker && (!webgl || g.enableLabels)) { this.marker = new ol.Feature(this.olPoint); this.marker.hex = `${this.icao}`; } if (webgl && !g.enableLabels && this.marker) { if (this.marker.visible) { PlaneIconFeatures.removeFeature(this.marker); this.marker.visible = false; } } if (webgl) { if (!this.glMarker) { this.glMarker = new ol.Feature(this.olPoint); this.glMarker.hex = `${this.icao}`; } this.setMarkerRgb(); const iconRotation = this.shape.noRotate ? 0 : this.rotation; this.glMarker.set('rotation', iconRotation * Math.PI / 180.0 + g.mapOrientation); this.glMarker.set('scale', this.scale * Math.max(this.shape.w, this.shape.h) / glIconSize); this.glMarker.set('sx', getSpriteX(this.shape) * glIconSize); this.glMarker.set('sy', getSpriteY(this.shape) * glIconSize); } if (this.marker && (!webgl || g.enableLabels)) { this.updateIcon(); if (!this.marker.visible) { this.marker.visible = true; PlaneIconFeatures.addFeature(this.marker); } } if (webgl && this.glMarker && !this.glMarker.visible) { this.glMarker.visible = true; webglFeatures.addFeature(this.glMarker); } if (!webgl && this.glMarker && this.glMarker.visible) { webglFeatures.removeFeature(this.glMarker); this.glMarker.visible = false; } }; // return the styling of the lines based on altitude function altitudeLines (segment) { let colorArr = altitudeColor(segment.altitude); if (segment.estimated) colorArr = [colorArr[0], colorArr[1], colorArr[2] * 0.8]; //let color = 'hsl(' + colorArr[0].toFixed(0) + ', ' + colorArr[1].toFixed(0) + '%, ' + colorArr[2].toFixed(0) + '%)'; let color = hslToRgb(colorArr); if (monochromeTracks) color = monochromeTracks; const modeS = (segment.dataSource == 'modeS'); const lineKey = '_' + color + debugTracks + noVanish + segment.estimated + newWidth + modeS + segment.noLabel + segment.estimatedFill; if (lineStyleCache[lineKey]) return lineStyleCache[lineKey]; let multiplier = segment.estimated ? 0.6 : 1; if (noVanish) multiplier *= (segment.estimated ? 0.3 : 0.6); let join = 'round'; let cap = 'square'; if (!debugTracks) { if (modeS) { lineStyleCache[lineKey] = [ new ol.style.Style({}), new ol.style.Style({ image: new ol.style.Circle({ radius: 3 * newWidth, fill: new ol.style.Fill({ color: color }) }), geometry: function(feature) { return new ol.geom.MultiPoint(feature.getGeometry().getCoordinates()); } }) ]; } else if (segment.estimated && !noVanish) { lineStyleCache[lineKey] = [ new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'black', width: 2 * newWidth * 0.3, lineJoin: join, lineCap: cap, }) }), new ol.style.Style({ stroke: new ol.style.Stroke({ color: color, width: 2 * newWidth, lineDash: [10, 20 + 3 * newWidth], lineDashOffset: 5, lineJoin: join, lineCap: cap, }), }) ]; } else if (segment.estimated && pTracks) { lineStyleCache[lineKey] = new ol.style.Style({}); } else { lineStyleCache[lineKey] = new ol.style.Style({ stroke: new ol.style.Stroke({ color: color, width: 2 * newWidth * multiplier, lineJoin: join, lineCap: cap, }) }); } } else { lineStyleCache[lineKey] = [ new ol.style.Style({ image: new ol.style.Circle({ radius: (segment.estimatedFill ? 0 : 2) * newWidth, fill: new ol.style.Fill({ color: color }) }), geometry: function(feature) { return new ol.geom.MultiPoint(feature.getGeometry().getCoordinates()); } }), new ol.style.Style({ stroke: new ol.style.Stroke({ color: color, width: ((segment.noLabel || segment.estimated) ? 0.5 : 1) * newWidth * multiplier, lineJoin: join, lineCap: cap, }) }) ]; } return lineStyleCache[lineKey]; } // Update our planes tail line, PlaneObject.prototype.updateLines = function() { this.drawLine = false; if (!this.visible || this.position == null || (!this.selected && !SelectedAllPlanes)) { if (this.linesDrawn) { this.clearLines(); } return; } this.linesDrawn = true; this.clearTraceAfter = null; if (this.track_linesegs.length == 0) return; this.checkLayers(); let trail_add = []; let label_add = []; if (!this.layer.getVisible()) this.layer.setVisible(true); if (trackLabels || showTrace) { if (!this.layer_labels.getVisible()) this.layer_labels.setVisible(true); } else if (this.layer_labels && this.layer_labels.getVisible()) { this.layer_labels.setVisible(false); } // create the new elastic band feature if (this.elastic_feature) { this.trail_features.removeFeature(this.elastic_feature); this.elastic_feature = null; } // create any missing fixed line features for (let i = this.track_linesegs.length-1; i >= 0; i--) { let seg = this.track_linesegs[i]; if (seg.feature && (!trackLabels || seg.label)) break; if ((filterTracks && altFiltered(seg.altitude)) || altitudeLines(seg) == nullStyle) { seg.feature = true; } else if (!seg.feature) { seg.feature = true; let feature = new ol.Feature(seg.fixed); feature.setStyle(altitudeLines(seg)); feature.hex = `${this.icao}`; feature.timestamp = Number(seg.ts); trail_add.push(feature); } if (seg.label) { // nothing to do, label already present } else if ((filterTracks && altFiltered(seg.altitude)) || seg.noLabel) { seg.label = true; } else if ( trackLabels || ((i == 0 || i == this.track_linesegs.length-1 ||seg.leg) && showTrace && g.enableLabels) ) { // 0 vertical rate to avoid arrow let altString; if(seg.alt_real == "ground") { altString = "Ground"; } else { let alt; if (labelsGeom) { alt = adjust_geom_alt(seg.alt_geom, seg.position); } else { alt = adjust_baro_alt(seg.alt_real); } if (alt == null) { altString = (NBSP+'?'+NBSP); } else { altString = format_altitude_brief(alt, 0, DisplayUnits, showLabelUnits); } } const speedString = (seg.speed == null) ? (NBSP+'?'+NBSP) : format_speed_brief(seg.speed, DisplayUnits, showLabelUnits).padStart(3, NBSP); seg.label = new ol.Feature(new ol.geom.Point(seg.fixed.getFirstCoordinate())); let timestamp1; let timestamp2 = ""; const historic = (showTrace || replay); const useLocal = ((historic && !utcTimesHistoric) || (!historic && !utcTimesLive)); const date = new Date(seg.ts * 1000); if (!date) { console.log(seg); } let refDate = showTrace ? traceDate : new Date(); if (replay) { refDate = replay.ts }; if (useLocal && historic) { timestamp1 = lDateString(date); timestamp1 += '\n'; } else if (getDay(refDate) == getDay(date)) { timestamp1 = ""; } else { if (useLocal) { timestamp1 = lDateString(date); } else { timestamp1 = zDateString(date); } timestamp1 += '\n'; } if (useLocal) { timestamp2 += localTime(date); } else { timestamp2 += zuluTime(date); } if (traces_high_res || debugTracks) { timestamp2 += '.' + (Math.floor((seg.ts*10)) % 10); } if (showTrace && !utcTimesHistoric) { timestamp2 += '\n' + TIMEZONE; } else if (!useLocal) { timestamp2 += NBSP + 'Z'; } let text = speedString.padStart(3, NBSP) + " " + altString.padStart(6, NBSP) + "\n" //+ NBSP + format_track_arrow(seg.track) + timestamp1 + timestamp2; if (seg.rId && show_rId) { text += "\n" + seg.rId.substring(0,9); //+ "\n" + seg.rId.substring(9,18); } if (showTrace && !trackLabels) text = timestamp1 + timestamp2; let fill = labelFill; let zIndex = -i - 50 * (seg.alt_real == null); if (seg.leg == 'start') { fill = new ol.style.Fill({color: '#88CC88' }); zIndex += 123499; } if (seg.leg == 'end') { fill = new ol.style.Fill({color: '#8888CC' }); zIndex += 123455; } const otherDiag = seg.track != null && ((seg.track > 270 && seg.track < 360) || (seg.track > 90 && seg.track < 180)); seg.label.setStyle( new ol.style.Style({ text: new ol.style.Text({ text: `${text}`, fill: fill, stroke: labelStroke, textAlign: 'left', //backgroundFill: bgFill, textBaseline: otherDiag ? 'bottom' : 'top', font: labelFont, offsetX: (otherDiag ? 4 : 8) * globalScale, offsetY: (otherDiag ? -4 : 8) * globalScale, }), image: new ol.style.Circle({ radius: 2 * globalScale, fill: blackFill, }), zIndex: Number(zIndex), }) ); seg.label.hex = `${this.icao}`; seg.label.timestamp = Number(seg.ts); seg.label.isLabel = true; label_add.push(seg.label) } } let lastseg = this.track_linesegs[this.track_linesegs.length - 1]; let lastfixed = lastseg.fixed.getCoordinateAt(1.0); let geom = new ol.geom.LineString([lastfixed, ol.proj.fromLonLat(this.position)]); if (!showTrace) { this.elastic_feature = new ol.Feature(geom); if (filterTracks && altFiltered(lastseg.altitude)) { this.elastic_feature.setStyle(nullStyle); } else { this.elastic_feature.setStyle(altitudeLines(lastseg)); } this.elastic_feature.hex = this.icao; trail_add.push(this.elastic_feature); } if (trail_add.length > 0) this.trail_features.addFeatures(trail_add); if (this.trail_labels && label_add.length > 0) this.trail_labels.addFeatures(label_add); }; PlaneObject.prototype.resetTrail = function() { this.removeTrail(); this.track_linesegs = []; } PlaneObject.prototype.removeTrail = function() { if (this.trail_features) this.trail_features.clear(); if (this.trail_labels) this.trail_labels.clear(); for (let i in this.track_linesegs) { delete this.track_linesegs[i].feature; delete this.track_linesegs[i].label; } this.elastic_feature = null; }; // This is to remove the line from the screen if we deselect the plane PlaneObject.prototype.clearLines = function() { this.linesDrawn = false; if (this.layer && this.layer.getVisible()) { this.layer.setVisible(false); } if (this.layer_labels && this.layer_labels.getVisible()) { this.layer_labels.setVisible(false); } }; PlaneObject.prototype.clearTrace = function() { this.clearTraceAfter = null; this.clearLines(); this.removeTrail(); if (globeIndex) { this.recentTrace = null; this.fullTrace = null; } } PlaneObject.prototype.destroyTrace = function() { this.clearTrace(); if (this.layer) { trailGroup.remove(this.layer); this.trail_features = null; this.layer = null; } if (this.layer_labels) { trailGroup.remove(this.layer_labels); this.trail_labels = null; this.layer_labels = null; } } PlaneObject.prototype.makeTR = function (trTemplate) { this.trCache = []; this.bgColorCache = undefined; this.tr = trTemplate; this.clickListener = (evt) => { if (evt.srcElement instanceof HTMLAnchorElement) { evt.stopPropagation(); return; } if(!mapIsVisible) { selectPlaneByHex(this.icao, {follow: true}); } else { selectPlaneByHex(this.icao, {follow: false}); } evt.preventDefault(); }; this.tr.addEventListener('click', this.clickListener); if (!globeIndex) { this.dblclickListener = (evt) => { if(!mapIsVisible) { showMap(); } selectPlaneByHex(this.icao, {follow: true}); evt.preventDefault(); }; this.tr.addEventListener('dblclick', this.dblclickListener); } }; PlaneObject.prototype.destroyTR = function (trTemplate) { if (this.tr == null) return; this.tr.removeEventListener('click', this.clickListener); this.tr.removeEventListener('dblclick', this.dblclickListener); delete this.clickListener; delete this.dblclickListener; if (this.tr.parentNode) { this.tr.parentNode.removeChild(this.tr); } this.tr.remove(); this.tr = null; }; PlaneObject.prototype.destroy = function() { this.clearLines(); this.clearMarker(); this.visible = false; deselect(this); this.destroyTR(); this.destroyTrace(); for (let key in Object.keys(this)) { delete this[key]; } }; function calcAltitudeRounded(altitude) { if (altitude == null) { return null; } else if (altitude == "ground") { return altitude; } else if (altitude > 8000 || heatmap) { return (altitude/500).toFixed(0)*500; } else { return (altitude/125).toFixed(0)*125; } }; PlaneObject.prototype.drawRedDot = function(bad_position) { this.checkLayers(); if (debugJump && loadFinished && SelectedPlane != this) { OLMap.getView().setCenter(ol.proj.fromLonLat(bad_position)); selectPlaneByHex(this.icao, false); } let badFeat = new ol.Feature(new ol.geom.Point(ol.proj.fromLonLat(bad_position))); badFeat.setStyle(this.dataSource == "mlat" ? badDotMlat : badDot); this.trail_features.addFeature(badFeat); let geom = new ol.geom.LineString([ol.proj.fromLonLat(this.prev_position), ol.proj.fromLonLat(bad_position)]); let lineFeat = new ol.Feature(geom); lineFeat.setStyle(this.dataSource == "mlat" ? badLineMlat : badLine); this.trail_features.addFeature(lineFeat); }; function hexToHSL(hex) { let r = +('0x'+ hex[1] + hex[2]) / 255; let g = +('0x'+ hex[3] + hex[4]) / 255; let b = +('0x'+ hex[5] + hex[6]) / 255; let cmin = Math.min(r,g,b); let cmax = Math.max(r,g,b); let delta = cmax - cmin; let h = 0, s = 0, l = 0; if (delta == 0) h = 0; else if (cmax == r) h = ((g - b) / delta) % 6; else if (cmax == g) h = (b - r) / delta + 2; else h = (r - g) / delta + 4; h = Math.round(h * 60); if (h < 0) h += 360; l = (cmax + cmin) / 2; s = delta == 0 ? 0 : delta / (1 - Math.abs(2 * l - 1)); s *= 100; l *= 100; return [h, s, l]; }; /** * Converts an HSL color value to RGB. Conversion formula * adapted from http://en.wikipedia.org/wiki/HSL_color_space. * Assumes h, s, and l are contained in the set [0, 1] and * returns r, g, and b in the set [0, 255]. * * @param {number} h The hue * @param {number} s The saturation * @param {number} l The lightness * @return {Array} The RGB representation */ function hslToRgb(arr, opacity){ let h = arr[0]; let s = arr[1]; let l = arr[2]; let r, g, b; h /= 360; s *= 0.01; l *= 0.01; if(s == 0){ r = g = b = l; // achromatic }else{ let hue2rgb = function hue2rgb(p, q, t){ if(t < 0) t += 1; if(t > 1) t -= 1; if(t < 1/6) return p + (q - p) * 6 * t; if(t < 1/2) return q; if(t < 2/3) return p + (q - p) * (2/3 - t) * 6; return p; } let q = l < 0.5 ? l * (1 + s) : l + s - l * s; let p = 2 * l - q; r = hue2rgb(p, q, h + 1/3); g = hue2rgb(p, q, h); b = hue2rgb(p, q, h - 1/3); } if (opacity == 'array') return [ Math.round(r * 255), Math.round(g * 255), Math.round(b * 255) ]; if (opacity != null) return 'rgba(' + Math.round(r * 255) + ', ' + Math.round(g * 255) + ', ' + Math.round(b * 255) + ', ' + opacity + ')'; else return 'rgb(' + Math.round(r * 255) + ', ' + Math.round(g * 255) + ', ' + Math.round(b * 255) + ')'; } PlaneObject.prototype.altBad = function(newAlt, oldAlt, oldTime, data) { let max_fpm = 12000; if (data.geom_rate != null) max_fpm = 1.3*Math.abs(data.goem_rate) + 5000; else if (data.baro_rate != null) max_fpm = 1.3*Math.abs(data.baro_rate) + 5000; const delta = Math.abs(newAlt - oldAlt); const fpm = (delta < 800) ? 0 : (60 * delta / (now - oldTime + 2)); return fpm > max_fpm; }; PlaneObject.prototype.getAircraftData = function() { if (0) { this.dbinfoLoaded = true; return; } if (this.dbLoad) { return; } this.dbLoad = true; let req = dbLoad(this.icao); req.then( data => { //console.log('fromDB'); if (this.dbinfoLoaded) return; this.dbinfoLoaded = true; delete this.dbLoad; if (data == null) { //console.log(this.icao + ': Not found in database!'); return; } if (data == "strange") { //console.log(this.icao + ': Database malfunction!'); return; } //console.log(this.icao + ': loaded!'); // format [r:0, t:1, f:2] if (data[1]) { this.icaoType = `${data[1]}`; this.setTypeData(); } if (data[3]) { this.typeLong = `${data[3]}`; } if (data[2]) { this.military = (data[2][0] == '1'); this.interesting = (data[2][1] == '1'); this.pia = (data[2][2] == '1'); this.ladd = (data[2][3] == '1'); if (this.pia) this.registration = null; } if (data[0]) { this.registration = `${data[0]}`; } this.dataChanged(); data = null; }, e => { delete this.dbLoad; if (e.http_status == 'timeout') { this.getAircraftData(); } else if (e.http_status == 'other') { this.dbinfoLoaded = true; } else { console.log(this.icao + ': Unrecognized Database load error: ' + e); this.dbinfoLoaded = true; } }); }; PlaneObject.prototype.reapTrail = function() { const oldSegs = this.track_linesegs; this.track_linesegs = []; this.history_size = 0; for (let i in oldSegs) { const seg = oldSegs[i]; if (seg.ts + tempTrailsTimeout > now) { this.history_size += seg.fixed.getCoordinates().length; this.track_linesegs.push(seg); } } if (this.track_linesegs.length != oldSegs.length) { this.removeTrail(); this.updateTick(true); } }; PlaneObject.prototype.milRange = function() { if (this.fakeHex) // non-icao hex return false; for (let i in milRanges) { const r = milRanges[i]; if (this.numHex >= r[0] && this.numHex <= r[1]) return true; } return false; }; PlaneObject.prototype.updateTraceData = function(state, _now) { const lat = state[1]; const lon = state[2]; const altitude = state[3]; const gs = state[4]; const track = state[5]; const rate_geom = state[6] & 4; const alt_geom = state[6] & 8; const rate = state[7]; const data = state[8]; const type = state[9]; const geom_alt = state[10]; const geom_rate = state[11]; const ias = state[12]; const roll = state[13]; const rId = state[14]; this.position = [lon, lat]; this.position_time = _now; this.last_message_time = _now; this.altitude = altitude; if (altitude && altitude != "ground" && this.geom_diff_ts && _now - this.geom_diff_ts < 60) { this.alt_geom = altitude + this.geom_diff; } this.updateAlt(); if (alt_geom) { this.alt_geom = altitude; //this.alt_baro = null; } else { this.alt_baro = altitude; //this.alt_geom = null; } this.speed = gs; this.gs = gs; if (altitude == 'ground') { this.true_heading = track; this.track = null; } else { this.track = track; //this.true_heading = null; } if (track) this.rotation = track; else this.request_rotation_from_track = true; this.vert_rate = rate; if (rate_geom) { this.geom_rate = rate; this.baro_rate = null; } else { this.baro_rate = rate; this.geom_rate = null; } if (geom_alt !== undefined) { this.alt_geom = geom_alt; } if (geom_rate !== undefined) { this.geom_rate = geom_rate; } if (roll !== undefined) this.roll = roll; if (ias !== undefined) this.ias = ias; if (type !== undefined) this.addrtype = (type == null) ? null : `${type}`; if (rId !== undefined) this.rId = rId; if (data != null) { this.setFlight(data.flight); if (data.alt_geom != null && !alt_geom && altitude != null && altitude != "ground") { //this.alt_geom = altitude + this.geom_diff; this.geom_diff = data.alt_geom - altitude; this.geom_diff_ts = _now; } this.addrtype = (data.type == null) ? null : `${data.type}`; this.alt_geom = data.alt_geom; this.ias = data.ias; this.tas = data.tas; this.track = data.track; this.mag_heading = data.mag_heading; this.true_heading = data.true_heading; this.mach = data.mach; this.track_rate = data.track_rate; this.roll = data.roll; this.nav_altitude = data.nav_altitude; this.nav_heading = data.nav_heading; this.nav_modes = data.nav_modes; this.nac_p = data.nac_p; this.nac_v = data.nac_v; this.nic_baro = data.nic_baro; this.sil_type = data.sil_type; this.sil = data.sil; this.nav_qnh = data.nav_qnh; this.baro_rate = data.baro_rate; this.geom_rate = data.geom_rate; this.rc = data.rc; this.squawk = (data.squawk == null) ? null : `${data.squawk}`; this.wd = data.wd; this.ws = data.ws; this.oat = data.oat; this.tat = data.tat; // fields with more complex behaviour this.version = data.version; if (data.category != null) { this.category = `${data.category}`; } if (data.nav_altitude_fms != null) { this.nav_altitude = data.nav_altitude_fms; } else if (data.nav_altitude_mcp != null){ this.nav_altitude = data.nav_altitude_mcp; } else { this.nav_altitude = null; } } if (!this.addrtype) { this.dataSource = "unknown"; } else if (this.addrtype.substring(0,4) == "adsb") { this.dataSource = "adsb"; } else if (this.addrtype.substring(0,4) == "adsr") { this.dataSource = "adsr"; } else if (this.addrtype == "mlat") { this.dataSource = "mlat"; } else if (this.addrtype == "adsb_icao_nt") { this.dataSource = "modeS"; } else if (this.addrtype == 'mode_s') { this.dataSource = "modeS"; } else if (this.addrtype.substring(0,4) == "tisb") { this.dataSource = "tisb"; } else if (this.addrtype == 'adsc') { this.dataSource = "adsc"; } else if (this.addrtype == 'other') { this.dataSource = "other"; } else if (this.addrtype == 'unknown') { this.dataSource = "unknown"; } }; function makeCircle(points, greyskull) { let out = points; //console.log('1: ' + out.map(x => [Math.round(x[0]), Math.round(x[1])])); for (let k = 0; k < greyskull; k++) { out = [points[0]]; for (let j = 1; j < points.length; j++) { let i = j - 1; out.push(midpoint(points[i], points[j])); out.push(points[j]); //console.log('added 2: ' + out.map(x => [Math.round(x[0]), Math.round(x[1])])); } points = out; } return out; } // adapted from https://github.com/seangrogan/great_circle_calculator function midpoint(from, to) { let lon1 = from[0] * (Math.PI/180); let lat1 = from[1] * (Math.PI/180); let lon2 = to[0] * (Math.PI/180); let lat2 = to[1] * (Math.PI/180); let b_x = Math.cos(lat2) * Math.cos(lon2 - lon1); let b_y = Math.cos(lat2) * Math.sin(lon2 - lon1); let lat3 = Math.atan2(Math.sin(lat1) + Math.sin(lat2), Math.sqrt((Math.cos(lat1) + b_x) * (Math.cos(lat1) + b_x) + b_y * b_y)); let lon3 = lon1 + Math.atan2(b_y, Math.cos(lat1) + b_x); lat3 /= (Math.PI/180); lon3 /= (Math.PI/180); lon3 = (lon3 + 540) % 360 - 180; return [lon3, lat3]; } PlaneObject.prototype.cross180 = function(on_ground, is_leg) { let sign1 = Math.sign(this.prev_position[0]); let sign2 = Math.sign(this.position[0]); let out = makeCircle([this.prev_position, this.position], 8); //console.log([...out]); let seg1 = []; let seg2 = []; let tmp; while ((tmp = out.shift()) != null) { if (sign1 == Math.sign(tmp[0])) seg1.push(tmp); else seg2.push(tmp); } //console.log([...seg1]); //console.log([...seg2]); let before = seg1[seg1.length - 1]; let after = seg2[0]; // weight according to the opposite distance, well longitude difference // not perfect, good enough let afterWeight = Math.abs(sign1 * 180 - before[0]); let beforeWeight = Math.abs(sign2 * 180 - after[0]); let midLat = (beforeWeight * before[1] + afterWeight * after[1]) / (beforeWeight + afterWeight); let midPoint1 = [sign1 * 180, midLat]; let midPoint2 = [sign2 * 180, midLat]; seg1.push(midPoint1); seg2.unshift(midPoint2); for (let i in seg1) seg1[i] = ol.proj.fromLonLat(seg1[i]); for (let i in seg2) seg2[i] = ol.proj.fromLonLat(seg2[i]); seg1.unshift(seg1[0]); this.track_linesegs.push({ fixed: new ol.geom.LineString([seg1.shift()]), feature: null, estimated: true, ground: (this.prev_alt == "ground"), altitude: this.prev_alt_rounded, alt_real: this.prev_alt, alt_geom: this.prev_alt_geom, position: this.prev_position, speed: this.prev_speed, ts: this.prev_time, track: this.prev_rot, leg: is_leg, rId: this.prev_rId, }); this.track_linesegs.push({ fixed: new ol.geom.LineString(seg1), feature: null, estimated: true, ground: (this.prev_alt == "ground"), altitude: this.prev_alt_rounded, alt_real: this.prev_alt, alt_geom: this.prev_alt_geom, position: this.prev_position, speed: this.prev_speed, track: this.prev_rot, ts: NaN, noLabel: true, rId: this.prev_rId, }); this.track_linesegs.push({ fixed: new ol.geom.LineString(seg2), feature: null, estimated: true, ground: (this.prev_alt == "ground"), altitude: this.prev_alt_rounded, alt_real: this.prev_alt, alt_geom: this.prev_alt_geom, position: this.prev_position, speed: this.prev_speed, track: this.prev_rot, ts: NaN, noLabel: true, rId: this.prev_rId, }); }; PlaneObject.prototype.dataChanged = function() { this.refreshTR = 0; if (useRouteAPI){ this.routeCheck(); } } PlaneObject.prototype.isNonIcao = function() { if (this.icao[0] == '~') return true; else return false; }; PlaneObject.prototype.checkVisible = function() { const refresh = g.lastRefreshInt / 1000; const noInfoTimeout = replay ? 600 : (reApi ? (30 + 2 * refresh) : (30 + Math.min(1, (globeTilesViewCount / globeSimLoad)) * (2 * refresh))); const modeSTime = (guessModeS && this.dataSource == "modeS") ? 300 : 0; const tisbReduction = (globeIndex && this.icao[0] == '~') ? 15 : 0; // If no packet in over 58 seconds, clear the plane. // Only clear the plane if it's not selected individually // recompute seen and seen_pos let __now = now; if (isNaN(__now)) { console.error("checkVisible: now is NaN, this is probably a browser bug: https://issues.chromium.org/issues/401652934"); __now = g.now; } if (this.dataSource == "uat") { __now = uat_now; } this.seen = Math.max(0, __now - this.last_message_time); this.seen_pos = Math.max(0, __now - this.position_time); this.noInfoTime = __now - this.last_info_server; let timeout = seenTimeout; if (this.dataSource == "mlat") { timeout = seenTimeoutMlat; } else if (this.dataSource == "adsc") { timeout = jaeroTimeout; } else if (this.dataSource == 'ais') { timeout = aisTimeout; } timeout += modeSTime - tisbReduction + refresh; const res = (!globeIndex || icaoFilter || this.inView || this.selected || SelectedAllPlanes) && ( (!globeIndex && this.seen < timeout) || (globeIndex && this.seen_pos < timeout && this.noInfoTime < noInfoTimeout) || this.selected || noVanish || (nogpsOnly && this.nogps && this.seen < 15 * 60) // ugly hard coded ); return res; }; PlaneObject.prototype.setTypeData = function() { if (g.type_cache == null || !this.icaoType || this.icaoType == this.icaoTypeCache) return; this.updateMarker(); this.icaoTypeCache = this.icaoType; let typeCode = this.icaoType.toUpperCase(); if (typeCode == 'P8 ?') { typeCode = 'P8'; } if (!(typeCode in g.type_cache)) return; let typeData = g.type_cache[typeCode]; const typeLong = typeData[0]; const desc = typeData[1]; const wtc = typeData[2]; if (desc != null) this.typeDescription = `${desc}`; if (wtc != null) this.wtc = `${wtc}`; if (this.typeLong == null && typeLong != null) this.typeLong = `${typeLong}`; }; PlaneObject.prototype.setTypeFlagsReg = function(data) { if (data.t && data.t != this.icaoType) { this.icaoType = `${data.t}`; this.setTypeData(); } if (data.dbFlags) { this.military = data.dbFlags & 1; this.interesting = data.dbFlags & 2; this.pia = data.dbFlags & 4; this.ladd = data.dbFlags & 8; if (this.pia) this.registration = null; } if (data.r) this.registration = `${data.r}`; } PlaneObject.prototype.checkForDB = function(data) { if (!this.dbinfoLoaded && this.icao >= 'ae6620' && this.icao <= 'ae6899') { this.icaoType = 'P8 ?'; this.setTypeData(); } if (data) { if (data.desc) this.typeLong = `${data.desc}`; if (data.ownOp) this.ownOp = `${data.ownOp}`; if (data.year) this.year = `${data.year}`; this.setTypeFlagsReg(data); if (data.r || data.t) { this.dbinfoLoaded = true; } } if (!this.dbinfoLoaded && (!dbServer || replay || pTracks || heatmap)) { this.getAircraftData(); return; } this.dataChanged(); }; PlaneObject.prototype.updateAlt = function(t) { this.alt_rounded = calcAltitudeRounded(this.altitude); if (this.altitude == null) { this.onGround = null; this.zIndex = 10; } else if (this.altitude == "ground") { this.onGround = true; this.zIndex = 5; } else { this.onGround = false; this.zIndex = this.altitude + 200000; } if (this.category == 'C3' || this.icaoType == 'TWR') { this.zIndex = 1; } if (this.fakeHex) this.zIndex -= 100000; } PlaneObject.prototype.setProjection = function(arg) { let pos = traceOpts.animate ? traceOpts.animatePos : this.position; let lon = pos[0]; let lat = pos[1]; let moved = false; //let trace = new Error().stack.toString(); //console.log(lat + ' ' + trace); // manual wrap around (no longer necessary due to OpenLayers changing their webGL code) if (0 && webgl && Math.abs(CenterLon - lon) > 180) { if (CenterLon < 0) lon -= 360; else lon += 360; } //console.log([lat, lon]); let proj = ol.proj.fromLonLat([lon, lat]); if (this == SelectedPlane && (arg == 'follow' || checkFollow())) { OLMap.getView().setCenter(proj); } if (this.proj) { moved |= (this.proj[0] != proj[0] || this.proj[1] != proj[1]); } else { moved = true; } this.proj = proj; if (!this.olPoint) { this.olPoint = new ol.geom.Point(proj); } else if (moved) { this.olPoint.setCoordinates(proj); } } function normalized_callsign(flight) { const re = /^([A-Z]*)([0-9]*)([A-Z]*)$/; const match = flight.match(re); if (!match) { return flight; } let alpha = match[1]; let num = match[2]; let alpha2 = match[3]; while(num[0] == '0' && num.length > 1) { num = num.slice(1); } return alpha + num + alpha2; } PlaneObject.prototype.routeCheck = function() { if (!this.visible) { // we don't care, don't update return; } if (!this.flight || !this.name || this.name == 'empty callsign' || this.name == 'no callsign' || this.registration == this.name ) { this.routeString = ''; this.routeVerbose = ''; return; } if (!this.position) { // don't update if no position return; } let currentName = normalized_callsign(this.name); if (g.route_check_todo[currentName]) { // already checking return; } let currentTime = new Date().getTime()/1000; // if we don't have a route cached or if the cache is older than 6h, do a lookup const route = g.route_cache[currentName]; if (!route || currentTime > route.tarNextUpdate) { // we have all the pieces that allow us to lookup a route let route_check = { 'callsign': currentName, 'lat': this.position[1], 'lng': this.position[0], icao: this.icao}; if ((showTrace || replay) && routeApiUrl.includes("adsb.im")) { delete route_check['lat']; delete route_check['lng']; } g.route_check_todo[currentName] = route_check; return; } if (!route._airports || route._airports.length < 1) { this.routeString = ''; this.routeVerbose = ''; return; } let routeString = ""; let cities = ""; for (let airport of route._airports) { if (routeString) { if (routeDisplay.includes('city')) { routeString += " -\n" } else { routeString += " - " } cities += " - "; } let aString = "" for (let type of routeDisplay) { if (aString) { aString += '/'; } if (type == 'iata') { aString += airport.iata; } else if (type == 'icao') { aString += airport.icao; } else if (type == 'city') { aString += airport.location; } } cities += airport.location; routeString += aString; } if (route._airports.length > 2) { this.routeColumn = 'MULTIHOP'; } else { this.routeColumn = routeString; } if (!route.plausible) { routeString = '?? ' + routeString; } //console.log(this.routeString); this.routeString = routeString; this.routeVerbose = cities; } function routeDoLookup(currentTime) { // JavaScript doesn't interrupt running functions - so this should be safe to do if (g.route_check_in_flight) { return; } if (!g.route_check_checking) { // grab up to the first 100 callsigns and leave the rest for later g.route_check_checking = Object.values(g.route_check_todo).slice(0,100); } if (g.route_check_checking.length < 1) { g.route_check_checking = null; return; } g.route_check_in_flight = true; if (debugRoute) { console.log(`${currentTime}: g.route_check_checking:`, g.route_check_checking); } const requestRoutes = []; for (const entry of g.route_check_checking) { requestRoutes.push({ callsign: entry['callsign'], lat: entry['lat'], lng: entry['lng'], }); } const requestBody = JSON.stringify({ 'planes': requestRoutes }); if (debugRoute) { console.log(`${currentTime}: requesting routes:`, requestBody); } jQuery.ajax({ type: "POST", url: routeApiUrl, contentType: 'application/json; charset=utf-8', dataType: 'json', data: requestBody, }) .done((routes) => { let currentTime = new Date().getTime()/1000; g.route_check_in_flight = false; if (debugRoute) { console.log(`${currentTime}: got routes:`, routes); } for (let i in routes) { const route = routes[i]; if (!route) { console.error(`Route API returned this invalid element: ${route}, probably for`, g.route_check_checking[i]); continue; } route.tarNextUpdate = currentTime + 6 * 3600; // recheck in 6 hours g.route_cache[route.callsign] = route; } for (const entry of g.route_check_checking) { delete g.route_check_todo[entry.callsign]; const cached = g.route_cache[entry.callsign]; if (!cached || cached.tarnextUpdate > currentTime) { g.route_cache[entry.callsign] = { tarNextUpdate: currentTime + 60 }; } const plane = g.planes[entry.icao]; plane && plane.dataChanged(); } // let's update the route data immediately by refreshing the interface refresh(); g.route_check_checking = null; }) .fail((jqxhr, status, error) => { g.route_check_in_flight = false; g.route_next_lookup = Date.now()/1000 + 15; console.log('route API request error, delaying next request by 15 seconds.'); }); } PlaneObject.prototype.setFlight = function(flight) { if (flight == null) { if (now - this.flightTs > 10 * 60) { this.flight = null; this.name ='no callsign'; } } else if (flight == "@@@@@@@@") { this.flight = null; this.name ='no callsign'; } else { this.flight = `${flight}`; this.name = this.flight.trim() || 'empty callsign'; this.flightTs = now; } } function normalizeTraceStamps(data) { if (!data || !data.trace) { console.log('normalizeTraceStamps: trace empty?') return null; } let trace = data.trace; let last = 0; let negOffsetWarned = 0; for (let i = 0; i < trace.length; i++) { let point = trace[i]; if (point[0] < 0 && !negOffsetWarned) { negOffsetWarned = 1; console.log('negative offset in trace'); } point[0] += data.timestamp; if (point[0] >= last) { last = point[0]; } else { console.log('normalize: trace backwards last: ' + last.toFixed(3) + ' current: ' + point[0].toFixed(3)); } } data.timestamp = 0; return data; }