ui_rtk-status.js

/**
 * RTK Status UI Component
 * 
 * This component displays the current RTK fix status, including fix quality,
 * satellite information, and correction data status.
 */
export class RtkStatus {
  /**
   * Create an RTK status component
   * @param {Object} options - Configuration options
   * @param {EventEmitter} options.events - Event emitter for communication
   * @param {string} options.selector - CSS selector for the container element
   */
  constructor(options = {}) {
    this.events = options.events;
    
    // Find container element if selector provided
    if (options.selector) {
      this.container = document.querySelector(options.selector);
    }
    
    // If no container, don't initialize UI
    if (!this.container) {
      console.warn('RtkStatus: No container element found. UI will not be initialized.');
      return;
    }
    
    // Current status information
    this.fixQuality = 0;
    this.satellitesUsed = 0;
    this.rtkMode = 'none'; // 'none', 'float', 'fixed'
    this.correctionAge = null;
    this.messagesReceived = 0;
    this.ggaRequired = false;
    this.ntripConnected = false;
    
    // Add CSS for proper styling
    this.addStyles();
    
    // Initialize UI
    this.initializeUI();
    
    // Register event listeners
    this.registerEventListeners();
    
    // Update status immediately
    this.updateStatus();
  }

  /**
   * Add required CSS styles to the document
   */
  addStyles() {
    // Check if styles already exist
    if (document.getElementById('rtk-status-styles')) {
      return;
    }
    
    // Create style element
    const style = document.createElement('style');
    style.id = 'rtk-status-styles';
    style.textContent = `
      .rtk-status-container {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
        background-color: #f7f7f7;
        border-radius: 5px;
        box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
        padding: 12px;
        margin-bottom: 15px;
      }
      
      .rtk-status-header {
        display: flex;
        align-items: center;
        margin-bottom: 8px;
      }
      
      .rtk-indicator {
        display: flex;
        align-items: center;
        font-weight: 500;
      }
      
      .rtk-indicator-icon {
        width: 14px;
        height: 14px;
        border-radius: 50%;
        margin-right: 8px;
      }
      
      .rtk-indicator-icon.no-fix { background-color: #9e9e9e; }
      .rtk-indicator-icon.autonomous { background-color: #ffcc00; }
      .rtk-indicator-icon.dgps { background-color: #ff9800; }
      .rtk-indicator-icon.rtk-float { background-color: #2196f3; }
      .rtk-indicator-icon.rtk-fixed { background-color: #4caf50; }
      
      .rtk-status-details {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 8px;
        margin-top: 10px;
        font-size: 13px;
      }
      
      .rtk-status-detail {
        display: flex;
        align-items: center;
      }
      
      .rtk-status-detail-icon {
        width: 12px;
        height: 12px;
        margin-right: 6px;
        opacity: 0.7;
      }
      
      .rtk-status-detail-label {
        color: #666;
        margin-right: 5px;
      }
      
      .rtk-status-detail-value {
        font-weight: 500;
      }
      
      .rtk-status-correction-age {
        color: #666;
      }
      
      .rtk-status-correction-age.stale {
        color: #ff9800;
      }
      
      .rtk-status-correction-age.old {
        color: #f44336;
      }
      
      .rtk-satellite-indicator {
        margin-top: 5px;
        height: 4px;
        display: flex;
        gap: 2px;
      }
      
      .rtk-satellite-bar {
        flex: 1;
        background-color: #e0e0e0;
        border-radius: 2px;
      }
      
      .rtk-satellite-bar.used {
        background-color: #4caf50;
      }
      
      @media (max-width: 480px) {
        .rtk-status-details {
          grid-template-columns: 1fr;
        }
      }
    `;
    
    // Add style to document
    document.head.appendChild(style);
  }

  /**
   * Initialize the UI elements
   */
  initializeUI() {
    if (!this.container) return;
    
    // Clear container
    this.container.innerHTML = '';
    
    // Create main container
    const statusContainer = document.createElement('div');
    statusContainer.className = 'rtk-status-container';
    
    // Create header with main indicator
    const header = document.createElement('div');
    header.className = 'rtk-status-header';
    
    // Create status indicator
    const indicator = document.createElement('div');
    indicator.className = 'rtk-indicator';
    
    const indicatorIcon = document.createElement('div');
    indicatorIcon.className = 'rtk-indicator-icon no-fix';
    
    const indicatorText = document.createElement('div');
    indicatorText.textContent = 'No Fix';
    
    indicator.appendChild(indicatorIcon);
    indicator.appendChild(indicatorText);
    
    header.appendChild(indicator);
    statusContainer.appendChild(header);
    
    // Create satellite indicator
    const satelliteIndicator = document.createElement('div');
    satelliteIndicator.className = 'rtk-satellite-indicator';
    
    // Create bars for satellites (up to 20)
    for (let i = 0; i < 20; i++) {
      const bar = document.createElement('div');
      bar.className = 'rtk-satellite-bar';
      satelliteIndicator.appendChild(bar);
    }
    
    statusContainer.appendChild(satelliteIndicator);
    
    // Create status details
    const details = document.createElement('div');
    details.className = 'rtk-status-details';
    
    // Satellites
    const satellitesDetail = document.createElement('div');
    satellitesDetail.className = 'rtk-status-detail';
    
    const satellitesIcon = document.createElement('div');
    satellitesIcon.className = 'rtk-status-detail-icon';
    satellitesIcon.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M5,18L8.5,13.5L11,16.5L14.5,12L19,18H5M5,12V10A5,5 0 0,1 10,5H12V3L16,6L12,9V7H10A3,3 0 0,0 7,10V12H5Z" /></svg>`;
    
    const satellitesLabel = document.createElement('div');
    satellitesLabel.className = 'rtk-status-detail-label';
    satellitesLabel.textContent = 'Satellites:';
    
    const satellitesValue = document.createElement('div');
    satellitesValue.className = 'rtk-status-detail-value';
    satellitesValue.textContent = '0';
    
    satellitesDetail.appendChild(satellitesIcon);
    satellitesDetail.appendChild(satellitesLabel);
    satellitesDetail.appendChild(satellitesValue);
    
    details.appendChild(satellitesDetail);
    
    // Mode
    const modeDetail = document.createElement('div');
    modeDetail.className = 'rtk-status-detail';
    
    const modeIcon = document.createElement('div');
    modeIcon.className = 'rtk-status-detail-icon';
    modeIcon.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M12,8A4,4 0 0,1 16,12A4,4 0 0,1 12,16A4,4 0 0,1 8,12A4,4 0 0,1 12,8M12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12A2,2 0 0,0 12,10M10,22C9.75,22 9.54,21.82 9.5,21.58L9.13,18.93C8.5,18.68 7.96,18.34 7.44,17.94L4.95,18.95C4.73,19.03 4.46,18.95 4.34,18.73L2.34,15.27C2.21,15.05 2.27,14.78 2.46,14.63L4.57,12.97L4.5,12L4.57,11L2.46,9.37C2.27,9.22 2.21,8.95 2.34,8.73L4.34,5.27C4.46,5.05 4.73,4.96 4.95,5.05L7.44,6.05C7.96,5.66 8.5,5.32 9.13,5.07L9.5,2.42C9.54,2.18 9.75,2 10,2H14C14.25,2 14.46,2.18 14.5,2.42L14.87,5.07C15.5,5.32 16.04,5.66 16.56,6.05L19.05,5.05C19.27,4.96 19.54,5.05 19.66,5.27L21.66,8.73C21.79,8.95 21.73,9.22 21.54,9.37L19.43,11L19.5,12L19.43,13L21.54,14.63C21.73,14.78 21.79,15.05 21.66,15.27L19.66,18.73C19.54,18.95 19.27,19.04 19.05,18.95L16.56,17.95C16.04,18.34 15.5,18.68 14.87,18.93L14.5,21.58C14.46,21.82 14.25,22 14,22H10M11.25,4L10.88,6.61C9.68,6.86 8.62,7.5 7.85,8.39L5.44,7.35L4.69,8.65L6.8,10.2C6.4,11.37 6.4,12.64 6.8,13.8L4.68,15.36L5.43,16.66L7.86,15.62C8.63,16.5 9.68,17.14 10.87,17.38L11.24,20H12.76L13.13,17.39C14.32,17.14 15.37,16.5 16.14,15.62L18.57,16.66L19.32,15.36L17.2,13.81C17.6,12.64 17.6,11.37 17.2,10.2L19.31,8.65L18.56,7.35L16.15,8.39C15.38,7.5 14.32,6.86 13.12,6.62L12.75,4H11.25Z" /></svg>`;
    
    const modeLabel = document.createElement('div');
    modeLabel.className = 'rtk-status-detail-label';
    modeLabel.textContent = 'Mode:';
    
    const modeValue = document.createElement('div');
    modeValue.className = 'rtk-status-detail-value';
    modeValue.textContent = 'None';
    
    modeDetail.appendChild(modeIcon);
    modeDetail.appendChild(modeLabel);
    modeDetail.appendChild(modeValue);
    
    details.appendChild(modeDetail);
    
    // Correction age
    const correctionDetail = document.createElement('div');
    correctionDetail.className = 'rtk-status-detail';
    
    const correctionIcon = document.createElement('div');
    correctionIcon.className = 'rtk-status-detail-icon';
    correctionIcon.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M19,22H5V20H19V22M17,10C15.58,10 14.26,10.77 13.55,12H13V7H16V5H13V2H11V5H8V7H11V12H10.45C9.74,10.77 8.42,10 7,10A4,4 0 0,0 3,14A4,4 0 0,0 7,18C8.42,18 9.74,17.23 10.45,16H13.55C14.26,17.23 15.58,18 17,18A4,4 0 0,0 21,14A4,4 0 0,0 17,10M7,16A2,2 0 0,1 5,14A2,2 0 0,1 7,12A2,2 0 0,1 9,14A2,2 0 0,1 7,16M17,16A2,2 0 0,1 15,14A2,2 0 0,1 17,12A2,2 0 0,1 19,14A2,2 0 0,1 17,16Z" /></svg>`;
    
    const correctionLabel = document.createElement('div');
    correctionLabel.className = 'rtk-status-detail-label';
    correctionLabel.textContent = 'Correction:';
    
    const correctionValue = document.createElement('div');
    correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age';
    correctionValue.textContent = 'N/A';
    
    correctionDetail.appendChild(correctionIcon);
    correctionDetail.appendChild(correctionLabel);
    correctionDetail.appendChild(correctionValue);
    
    details.appendChild(correctionDetail);
    
    // Messages
    const messagesDetail = document.createElement('div');
    messagesDetail.className = 'rtk-status-detail';
    
    const messagesIcon = document.createElement('div');
    messagesIcon.className = 'rtk-status-detail-icon';
    messagesIcon.innerHTML = `<svg viewBox="0 0 24 24" width="12" height="12"><path fill="currentColor" d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z" /></svg>`;
    
    const messagesLabel = document.createElement('div');
    messagesLabel.className = 'rtk-status-detail-label';
    messagesLabel.textContent = 'Messages:';
    
    const messagesValue = document.createElement('div');
    messagesValue.className = 'rtk-status-detail-value';
    messagesValue.textContent = '0';
    
    messagesDetail.appendChild(messagesIcon);
    messagesDetail.appendChild(messagesLabel);
    messagesDetail.appendChild(messagesValue);
    
    details.appendChild(messagesDetail);
    
    statusContainer.appendChild(details);
    
    // Add to container
    this.container.appendChild(statusContainer);
    
    // Store references to elements
    this.elements = {
      indicatorIcon,
      indicatorText,
      satelliteIndicator,
      satellitesValue,
      modeValue,
      correctionValue,
      messagesValue
    };
  }

  /**
   * Register event listeners for GNSS and NTRIP events
   */
  registerEventListeners() {
    if (!this.events) {
      console.warn('RtkStatus: No events emitter provided. Status will not update.');
      return;
    }
    
    // GNSS position events
    this.events.on('position', this.handlePosition.bind(this));
    
    // NTRIP correction events
    this.events.on('ntrip:rtcm', this.handleRtcmData.bind(this));
    
    // NTRIP status events
    this.events.on('ntrip:connected', this.handleNtripConnected.bind(this));
    this.events.on('ntrip:disconnected', this.handleNtripDisconnected.bind(this));
    
    // Update status periodically
    setInterval(() => {
      this.updateStatus();
    }, 1000);
  }

  /**
   * Handle position update event
   * @param {Object} position - Position data
   */
  handlePosition(position) {
    // Update satellites used
    if (position.satellites !== undefined) {
      this.satellitesUsed = position.satellites;
    }
    
    // Update fix quality
    if (position.quality !== undefined) {
      this.fixQuality = position.quality;
    }
    
    // Update UI
    this.updateStatus();
  }

  /**
   * Handle RTCM data event
   * @param {Object} data - RTCM data and stats
   */
  handleRtcmData(data) {
    if (!data.stats) return;
    
    // Update message count
    this.messagesReceived = data.stats.messagesReceived;
    
    // Update correction age
    if (data.stats.correctionAge !== undefined) {
      this.correctionAge = data.stats.correctionAge;
    }
    
    // Update UI
    this.updateStatus();
  }

  /**
   * Handle NTRIP connected event
   * @param {Object} data - Connection data
   */
  handleNtripConnected(data) {
    // Reset stats
    this.messagesReceived = 0;
    this.correctionAge = null;
    this.ntripConnected = true;
    
    // Check if GGA is required
    if (data.requiresGga) {
      this.ggaRequired = true;
      
      // If we have a position, make sure it's sent to NTRIP
      if (this.gnss.lastPosition && this.gnss.ntripClient) {
        setTimeout(() => {
          this.gnss.updateNtripPosition(this.gnss.lastPosition);
        }, 500);
      }
    }
    
    // Update UI
    this.updateStatus();
  }

  /**
   * Handle NTRIP disconnected event
   * @param {Object} data - Disconnection data
   */
  handleNtripDisconnected(data) {
    // Clear correction age
    this.correctionAge = null;
    this.ntripConnected = false;
    this.ggaRequired = false;
    
    // Update UI
    this.updateStatus();
  }

  /**
   * Update the status display
   */
  updateStatus() {
    if (!this.elements) return;
    
    // Update fix quality indicator
    this.updateFixQualityIndicator();
    
    // Update satellite count
    this.elements.satellitesValue.textContent = this.satellitesUsed.toString();
    
    // Update satellite bars
    this.updateSatelliteBars();
    
    // Update RTK mode
    this.elements.modeValue.textContent = this.getRtkModeName();
    
    // Update correction age
    if (this.correctionAge !== null) {
      this.elements.correctionValue.textContent = `${this.correctionAge.toFixed(1)}s`;
      
      // Highlight old corrections
      if (this.correctionAge > 10) {
        this.elements.correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age old';
      } else if (this.correctionAge > 5) {
        this.elements.correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age stale';
      } else {
        this.elements.correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age';
      }
    } else if (this.ntripConnected && this.ggaRequired) {
      // Special message if connected to NTRIP but no corrections received and GGA is required
      this.elements.correctionValue.textContent = 'Waiting for GGA';
      this.elements.correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age stale';
    } else if (this.ntripConnected) {
      this.elements.correctionValue.textContent = 'Waiting for data';
      this.elements.correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age';
    } else {
      this.elements.correctionValue.textContent = 'N/A';
      this.elements.correctionValue.className = 'rtk-status-detail-value rtk-status-correction-age';
    }
    
    // Update message count
    this.elements.messagesValue.textContent = this.messagesReceived.toString();
  }

  /**
   * Update the fix quality indicator
   */
  updateFixQualityIndicator() {
    let indicatorClass = 'no-fix';
    let indicatorText = 'No Fix';
    
    switch (this.fixQuality) {
      case 1:
        indicatorClass = 'autonomous';
        indicatorText = 'Autonomous Fix';
        this.rtkMode = 'none';
        break;
      case 2:
        indicatorClass = 'dgps';
        indicatorText = 'DGPS';
        this.rtkMode = 'none';
        break;
      case 4:
        indicatorClass = 'rtk-fixed';
        indicatorText = 'RTK Fixed';
        this.rtkMode = 'fixed';
        break;
      case 5:
        indicatorClass = 'rtk-float';
        indicatorText = 'RTK Float';
        this.rtkMode = 'float';
        break;
      default:
        indicatorClass = 'no-fix';
        indicatorText = 'No Fix';
        this.rtkMode = 'none';
        break;
    }
    
    this.elements.indicatorIcon.className = `rtk-indicator-icon ${indicatorClass}`;
    this.elements.indicatorText.textContent = indicatorText;
  }

  /**
   * Update the satellite bars
   */
  updateSatelliteBars() {
    // Get all satellite bars
    const bars = this.elements.satelliteIndicator.children;
    
    // Update up to the available satellites
    for (let i = 0; i < bars.length; i++) {
      if (i < this.satellitesUsed) {
        bars[i].className = 'rtk-satellite-bar used';
      } else {
        bars[i].className = 'rtk-satellite-bar';
      }
    }
  }

  /**
   * Get RTK mode name
   * @returns {string} RTK mode name
   */
  getRtkModeName() {
    switch (this.rtkMode) {
      case 'fixed':
        return 'RTK Fixed';
      case 'float':
        return 'RTK Float';
      default:
        return 'Standalone';
    }
  }
}

export default RtkStatus;