Source: aisgps.js

/** Global AIS datas */
let aisData = null;

/** Global layer group to manage AIS markers */
let aisLayer = null;

/** The current GPS marker */
let gpsMarker = null;

/** Array of GPS coordinates for the track */
let gpsTrack = [];

/** The polyline showing the GPS track */
let gpsPolyline = null;

/**
 * Displays the current GPS position on the map.
 * Updates the marker, popup, and the track polyline.
 * 
 * @param {Object} gpsData - The GPS data in JSON format.
 * @param {number} gpsData.lat - Latitude in decimal.
 * @param {number} gpsData.lon - Longitude in decimal.
 * @param {string} gpsData.time - UTC time string.
 * @param {number} [gpsData["alt M"]] - Altitude in meters (optional).
 * @param {number} gpsData.sog - Speed over ground.
 * @param {number} gpsData.cog - Course over ground.
 * @param {number} gpsData.numSat - Number of satellites.
 */
function showBoatPosition (gpsData) {
   const lat = gpsData.lat;
   const lon = gpsData.lon;
   const dms = latLonToStr (lat, lon, DMSType);

   const altStr = gpsData["alt m"] !== undefined
      ? `Altitude: ${gpsData["alt m"]} m<br>`
      : "";

   const popupContent = `
      <b>GPS</b><br>
      Coordinates: ${dms}<br>
      ${altStr}
      SOG: ${gpsData.sog} kn<br>
      COG: ${gpsData.cog}°<br>
      Satellites: ${gpsData.numSat}<br>
      Time: ${gpsData.time}
   `;

   // Emoji icon for the marker
   const emojiIcon = L.divIcon({
      html: '📍',
      iconSize: [24, 24],
      className: 'gps-icon'
   });

   // Remove previous marker if exists
   if (gpsMarker) {
      map.removeLayer(gpsMarker);
   }
   if (gpsData.time === "NA" || gpsData.numSat < 2)
      return;

   // Add new marker with popup
   gpsMarker = L.marker([lat, lon], { icon: emojiIcon })
      .addTo(map)
      .bindPopup(popupContent);
      //.openPopup();

   // Add point to GPS track
   gpsTrack.push([lat, lon]);

   // Update or create the polyline
   /*if (gpsPolyline) {
      gpsPolyline.setLatLngs(gpsTrack);
   } else {
      gpsPolyline = L.polyline(gpsTrack, { color: 'red' }).addTo(map);
   }*/
   // Optional: center the map on the position
   // map.setView([lat, lon]);
}

/**
 * Fetches the current GPS position from the local server.
 * Uses a timeout to prevent hanging if the server is unresponsive.
 */
function fetchGpsPosition() {
   const controller = new AbortController();
   const timeout = setTimeout(() => {
      controller.abort(); // Cancel request after 5s
   }, 5000);

   fetch (gpsUrl, {
      method: 'GET',
      signal: controller.signal
   })
   .then(response => {
      clearTimeout(timeout);
      console.log('Server response status:', response.status);
      if (!response.ok) {
         throw new Error(`HTTP error ${response.status}`);
      }
      return response.text(); // read as text first to see raw output
   })
   .then(text => {
      console.log('Raw response from server:', text);

      let data;
      try {
         data = JSON.parse(text);
      } catch (e) {
         console.error('Failed to parse JSON:', e.message);
         if (gpsMarker) map.removeLayer(gpsMarker);
         return;
      }

      console.log('Parsed JSON:', data);
      showBoatPosition(data);
   })
   .catch(err => {
      clearTimeout(timeout);
      console.error('Fetch error:', err.message);
      if (gpsMarker) map.removeLayer(gpsMarker);
   });
}

/**
 * Display AIS data in a Swal2 modal as a spreadsheet-like table.
 * @param {Object[]} aisData - Array of AIS objects to display.
 */
function aisDump (aisData) {
   if (aisData === null) {
      Swal.fire ('AIS Warning', 'No AIS Data', 'warning');
      return;
   }
   // Generate table headers
   const headers = ['Name', 'MessageID', 'Country', 'Min Dist', 'MMSI', 'Coordinate', 'SOG', 'COG', 'Last Update'];
  
   // Build table HTML
   let html = '<div style="overflow-x:auto;"><table style="width:100%; border-collapse:collapse; text-align:left;">';
   html += '<thead><tr>';
   for (const header of headers) {
      html += `<th style="border:1px solid #ccc; padding:4px;">${header}</th>`;
   }
   html += '</tr></thead><tbody>';

   // Populate rows
   for (const item of aisData) {
      const cog = item.cog < 0 ? item.cog + 360 : item.cog;
      html += '<tr>';
      html += `<td style="border:1px solid #ccc; padding:4px;">${item.name}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${item.messageId}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${item.country}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${item.mindist}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${item.mmsi}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${latLonToStr (item.lat, item.loni, DMSType)}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${item.sog.toFixed(2).toString().padStart(5, '0')}</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${cog.toString().padStart(3, '0')}°</td>`;
      html += `<td style="border:1px solid #ccc; padding:4px;">${epochToStrDate(item.lastupdate)}</td>`;
      html += '</tr>';
   }

   html += '</tbody></table></div>';

   // Show with Swal2
   Swal.fire({
     title: 'AIS Dump',
     html: html,
     width: '80%',
     confirmButtonText: 'Close'
   });
}

/**
 * Show AIS targets on the Windy map, replacing previous ones.
 * @param {Object[]} aisData - Array of AIS data objects.
 */
function showAIS(aisData) {
   if (aisLayer) {
      map.removeLayer(aisLayer);
   }

   aisLayer = L.layerGroup();

   for (const boat of aisData) {
      if (boat.name === "_Unsupported") continue;
      const rotation = boat.cog; // Heading in degrees
      const isMoving = boat.sog > 0;

      // Triangle icon via divIcon + CSS transform
      const icon = L.divIcon({
         className: '', // no default class
         html: `
            <div class="${isMoving ? 'ais-moving' : ''}" style="
            --cog: ${rotation}deg;
            width: 0; height: 0;
            border-left: 4px solid transparent;
            border-right: 4px solid transparent;
            border-bottom: 18px solid orange;
            transform: rotate(${rotation}deg);
            transform-origin: center;
            "></div>
            `,
         iconSize: [12, 12],
         iconAnchor: [6, 6]
      });

      const marker = L.marker([boat.lat, boat.lon], { icon });
      const cog = (boat.cog < 0) ? boat.cog + 360 : boat.cog;
      const popupContent = `
         <b>${boat.name}</b><br>
         Country: ${boat.country}<br>
         MinDist: ${boat.mindist} Kn<br>
         MMSI: ${boat.mmsi}<br>
         Coordinate: ${latLonToStr (boat.lat, boat.lon, DMSType)}<br>
         SOG: ${boat.sog.toFixed(2)} Kn<br>
         COG: ${cog}°<br>
         LastUpdate: ${epochToStrDate(boat.lastupdate)}
      `;

      marker.bindPopup(popupContent);
      aisLayer.addLayer(marker);
   }   
   aisLayer.addTo(map);
}

/**
 * Fetches the current AIS information from the local server.
 * Uses a timeout to prevent hanging if the server is unresponsive.
 */
function fetchAisPosition() {
   const controller = new AbortController();
   const timeout = setTimeout(() => {
      controller.abort(); // Cancel request after 5s
   }, 5000);

   fetch (aisUrl, {
      method: 'GET',
      signal: controller.signal
   })
   .then(response => {
      clearTimeout(timeout);
      console.log('Server response status:', response.status);
      if (!response.ok) {
         throw new Error(`HTTP error ${response.status}`);
      }
      return response.text(); // read as text first to see raw output
   })
   .then(text => {
      console.log('Raw response from server:', text);

      let data;
      try {
         data = JSON.parse(text);
      } catch (e) {
         console.error('Failed to parse JSON:', e.message);
         if (gpsMarker) map.removeLayer(gpsMarker);
         return;
      }

      console.log('Parsed JSON:', data);
      aisData = data;
      showAIS (data);
   })
   .catch(err => {
      clearTimeout(timeout);
      console.error('Fetch error:', err.message);
      if (gpsMarker) map.removeLayer(gpsMarker);
   });
}