Source: r3waypoints.js

const EARTH_RADIUS_NM = 3440.065; // Earth average radius in Nautical Miles
const DEG_TO_RAD = Math.PI / 180;
const RAD_TO_DEG = 180 / Math.PI;

/**
 * Computes the initial orthodromic (great-circle) bearing from one point to another.
 *
 * This function calculates the initial heading (bearing) in degrees required 
 * to travel from the first point (lat0, lon0) to the second (lat1, lon1) along a great-circle route.
 * 
 * @param {number} lat0 - The latitude starting point
 * @param {number} lon0 - The longitude starting point
 * @param {number} lat1 - The latitude destination point
 * @param {number} lon1 - The longitude destination point
 * @returns {number} The initial bearing in degrees, ranging from 0 to 360°.
 */
function orthoCap (lat0, lon0, lat1, lon1) {
   lat0 *= DEG_TO_RAD;
   lon0 *= DEG_TO_RAD;
   lat1 *= DEG_TO_RAD;
   lon1 *= DEG_TO_RAD;
   
   const dLon = lon1 - lon0;
   const y = Math.sin(dLon) * Math.cos(lat1);
   const x = Math.cos(lat0) * Math.sin(lat1) - Math.sin(lat0) * Math.cos(lat1) * Math.cos(dLon);
   const heading = Math.atan2(y, x);

   return ((RAD_TO_DEG * heading) + 360) % 360;
}

/**
 * Calculates the orthodromic (great-circle) distance between two points.
 *
 * @param {number} lat0 - The latitude starting point
 * @param {number} lon0 - The longitude starting point
 * @param {number} lat1 - The latitude destination point
 * @param {number} lon1 - The longitude destination point
 * @returns {number} Distance in nautical miles.
 */
function orthoDist (lat0, lon0, lat1, lon1) {
   lat0 *= DEG_TO_RAD;
   lon0 *= DEG_TO_RAD;
   lat1 *= DEG_TO_RAD;
   lon1 *= DEG_TO_RAD;
   
   const deltaSigma = Math.acos(
      Math.sin(lat0) * Math.sin(lat1) +
      Math.cos(lat0) * Math.cos(lat1) * Math.cos(lon1 - lon0)
   );

   return EARTH_RADIUS_NM * deltaSigma;
}

/**
 * Computes the initial loxodromic (rhumb line) bearing from one point to another.
 *
 * @param {number} lat0 - The latitude starting point
 * @param {number} lon0 - The longitude starting point
 * @param {number} lat1 - The latitude destination point
 * @param {number} lon1 - The longitude destination point
 * @returns {number} Initial bearing in degrees from 0 to 360.
 */
function loxoCap (lat0, lon0, lat1, lon1) {
   lat0 *= DEG_TO_RAD;
   lon0 *= DEG_TO_RAD;
   lat1 *= DEG_TO_RAD;
   lon1 *= DEG_TO_RAD;
   let dLon = lon1 - lon0;

   const dPhi = Math.log(Math.tan(Math.PI / 4 + lat1 / 2) / Math.tan(Math.PI / 4 + lat0 / 2));

   // Correct dLon for antimeridian cross
   if (Math.abs(dLon) > Math.PI) {
      dLon = dLon > 0 ? -(2 * Math.PI - dLon) : (2 * Math.PI + dLon);
   }

   let bearing = Math.atan2(dLon, dPhi) * RAD_TO_DEG;
   return (bearing + 360) % 360;
}

/**
 * Calculates the loxodromic (rhumb line) distance between two points.
 *
 * @param {number} lat0 - The latitude starting point
 * @param {number} lon0 - The longitude starting point
 * @param {number} lat1 - The latitude destination point
 * @param {number} lon1 - The longitude destination point
 * @returns {number} Distance in nautical miles.
 */
function loxoDist (lat0, lon0, lat1, lon1) {
   lat0 *= DEG_TO_RAD;
   lon0 *= DEG_TO_RAD;
   lat1 *= DEG_TO_RAD;
   lon1 *= DEG_TO_RAD;
   let dLon = Math.abs(lon1 - lon0);
   const dLat = lat1 - lat0;

   const dPhi = Math.log(Math.tan(Math.PI / 4 + lat1 / 2) / Math.tan(Math.PI / 4 + lat0 / 2));

   const q = Math.abs(dPhi) > 1e-12 ? dLat / dPhi : Math.cos(lat0);

   // Correct dLon for antimeridian cross
   if (dLon > Math.PI) {
      dLon = 2 * Math.PI - dLon;
   }

   const distance = Math.sqrt(dLat * dLat + q * q * dLon * dLon);
   return EARTH_RADIUS_NM * distance;
}

/**
 * Displays waypoints and computed distances/courses for each competitor.
 *
 * For each competitor, this function builds a table that includes, for each leg between waypoints:
 * the index, lat/lon, orthodromic course and distance, and loxodromic course and distance.
 * Totals for distances are shown at the bottom in bold red.
 *
 * @param {Array<Object>} competitors - Array of competitors, each having at least `name`, `lat`, `lon`.
 * @param {Array<Array<number>>} wayPoints - Array of waypoints, each being [lat, lon].
 */
function displayWayPoints(competitors, wayPoints) {
   if (!Array.isArray(wayPoints) || wayPoints.length === 0) {
      Swal.fire({
         icon: "warning",
         title: "No waypoint defined",
         text: "Please define at least one waypoint before displaying data.",
      });
      return;
   }

   const container = document.createElement("div");

   competitors.forEach((competitor, index) => {
      const wrapper = document.createElement("div");
      wrapper.style.border = "1px solid #ccc";
      wrapper.style.borderRadius = "8px";
      wrapper.style.padding = "10px";
      wrapper.style.margin = "0 10px 20px 10px"; // marges gauche/droite
      wrapper.style.backgroundColor = "#f9f9f9";

      const table = document.createElement("table");
      table.style.borderCollapse = "collapse";
      table.style.width = "100%";
      table.style.textAlign = "right"; // alignement des nombres à droite
      table.style.margin = "auto";

      const caption = document.createElement("caption");
      caption.textContent = competitor.name;
      caption.style.fontWeight = "bold";
      caption.style.fontSize = "1.2em";
      caption.style.marginBottom = "0.5em";
      table.appendChild(caption);

      const thead = document.createElement("thead");
      thead.innerHTML = `
         <tr>
            <th style="text-align:left;">Waypoint</th>
            <th>Coord.</th>
            <th>Ortho Cap (°)</th>
            <th>Ortho Dist (NM)</th>
            <th>Loxo Cap (°)</th>
            <th>Loxo Dist (NM)</th>
         </tr>`;
      table.appendChild(thead);

      const tbody = document.createElement("tbody");

      let totalOrtho = 0;
      let totalLoxo = 0;
      let copyText = "Waypoint\tCoord.\tOrtho Cap (°)\tOrtho Dist (NM)\tLoxo Cap (°)\tLoxo Dist (NM)\n";

      let prevLat = competitor.lat, prevLon = competitor.lon;
      console.log ("N waypoints: "+ wayPoints.length);
      for (let i = 0; i < wayPoints.length; i++) {
         const strCoord = latLonToStr (wayPoints [i][0], wayPoints [i][1], DMSType);
         const orthoC = orthoCap(prevLat, prevLon, wayPoints [i][0], wayPoints [i][1]).toFixed(1);
         const orthoD = orthoDist(prevLat, prevLon, wayPoints [i][0], wayPoints [i][1]).toFixed(2); 
         console.log (orthoC, orthoD, prevLat, prevLon, wayPoints [i][0], wayPoints [i][1]);
         const loxoC = loxoCap(prevLat, prevLon, wayPoints [i][0], wayPoints [i][1]).toFixed(1);
         const loxoD = loxoDist(prevLat, prevLon, wayPoints [i][0], wayPoints [i][1]).toFixed(2);

         totalOrtho += parseFloat(orthoD);
         totalLoxo += parseFloat(loxoD);

         const label = (i === wayPoints.length - 1) ? "Dest." : i;
         const row = document.createElement("tr");
         row.innerHTML = `
            <td style="text-align:left;">${label}</td>
            <td>${strCoord}</td>
            <td>${orthoC}</td>
            <td>${orthoD}</td>
            <td>${loxoC}</td>
            <td>${loxoD}</td>`;
         tbody.appendChild(row);

         copyText += `${label}\t${strCoord}\t${orthoC}\t${orthoD}\t${loxoC}\t${loxoD}\n`;

         prevLat = wayPoints [i][0], prevLon = wayPoints [i][1];
      }

      // Totals row
      const totalRow = document.createElement("tr");
      totalRow.innerHTML = `
         <td style="font-weight:bold; color:red; text-align:left;">Total Distance</td>
         <td></td>
         <td></td>
         <td style="font-weight:bold; color:red;">${totalOrtho.toFixed(2)}</td>
         <td></td>
         <td style="font-weight:bold; color:red;">${totalLoxo.toFixed(2)}</td>`;
      tbody.appendChild(totalRow);

      copyText += `Total Distance\t\t\t${totalOrtho.toFixed(2)}\t\t${totalLoxo.toFixed(2)}\n`;

      table.appendChild(tbody);
      wrapper.appendChild(table);

      // Copy button
      const copyButton = document.createElement("button");
      copyButton.textContent = "Copy Table";
      copyButton.style.marginTop = "10px";
      copyButton.style.padding = "6px 12px";
      copyButton.style.border = "1px solid #999";
      copyButton.style.borderRadius = "4px";
      copyButton.style.backgroundColor = "#eee";
      copyButton.style.cursor = "pointer";
      copyButton.addEventListener("click", () => {
         navigator.clipboard.writeText(copyText).then(() => {
            Swal.fire({
               toast: true,
               position: 'top-end',
               icon: 'success',
               title: `Table copied for ${competitor.name}`,
               showConfirmButton: false,
               timer: 1500
            });
         });
      });

      wrapper.appendChild(copyButton);
      container.appendChild(wrapper);
   });

   Swal.fire({
      title: "Waypoint Distances",
      html: container,
      width: "90%",
      scrollbarPadding: false,
      confirmButtonText: "Close"
   });
}