"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) {
            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;
}
