//
//     Copyright © 2011-2025 Cambridge Intelligence Limited.
//     All rights reserved.
//
//     Sample Code
//!    Find the shortest path between two nodes.
import KeyLines from "keylines";
import { data, colours, styles } from "./findpath-data.js";

let chart;
const infoFields = document.getElementsByClassName("info");
let pathEnds = [null, null];
let pathWeighting = document.querySelector('input[name="option"]:checked')
  .value;
let clearAnimation = [];

function formatDuration(number) {
  const minsAsNumber = number % 60;
  const hours = (number - minsAsNumber) / 60;
  const minsAsTwoDigits = minsAsNumber < 10 ? `0${minsAsNumber}` : minsAsNumber;
  return `${hours}:${minsAsTwoDigits}`;
}

// update the sidebar to show info about the path
function updateSideInfo(pathLength, pathDistance) {
  infoFields[0].innerHTML = pathEnds[0];
  infoFields[1].innerHTML = pathEnds[1];
  infoFields[2].innerHTML = "&nbsp;";
  // user clicked a link (undefined) or 2 nodes that have no path between them (-0.5)
  if (pathLength < 0 || pathLength === undefined) {
    if (pathLength < 0) infoFields[2].innerHTML = "No available path";
    infoFields[3].innerHTML = "N/A";
    infoFields[4].innerHTML = "N/A";
  } else if (pathWeighting === "distance") {
    infoFields[3].innerHTML = pathLength;
    infoFields[4].innerHTML = `${pathDistance}km`;
  } else if (pathWeighting === "time") {
    infoFields[3].innerHTML = pathLength;
    infoFields[4].innerHTML = `${formatDuration(pathDistance)}hrs`;
  } else if (pathWeighting === "equal") {
    infoFields[3].innerHTML = pathLength;
    infoFields[4].innerHTML = `${pathDistance} links`;
  } else {
    Array.from(infoFields).forEach((element) => {
      element.innerHTML = "";
    });
  }
}

async function animateShortestPath(path, weighting) {
  for (const [index, item] of path.onePath.entries()) {
    if (index % 2 === 0) {
      // item is a node, so update border & enlargement
      await chart.animateProperties(
        { id: item, b: colours[weighting], ...styles.pathNode },
        { time: 30 }
      );
      clearAnimation.push({ id: item, ...styles.node });
    } else {
      // item is a link, so update colour and width
      await chart.animateProperties(
        {
          id: item,
          c: colours[weighting],
          border: { colour: colours[weighting], width: 2, radius: 3 },
          ...styles.pathLink,
        },
        { time: 30 }
      );
      clearAnimation.push({ id: item, ...styles.link });
    }
  }
}

function selectionChanged(node1, node2) {
  chart.animateProperties(clearAnimation, { time: 30 });
  clearAnimation = [];

  // if we have 2 path ends, calculate the path and animate it
  if (node1 && node2) {
    const options = { direction: "any" };
    if (pathWeighting !== "equal") options.value = pathWeighting;
    const path = chart.graph().shortestPaths(node1, node2, options);

    animateShortestPath(path, pathWeighting);
    updateSideInfo((path.onePath.length - 1) / 2, path.distance);
  }
}

function selectNode({ id }) {
  function resetSelection() {
    chart.selection({});
    pathEnds = [null, null];
    selectionChanged();
    updateSideInfo();
  }
  const validatedItem = chart.getItem(id);

  if (validatedItem && validatedItem.type === "node") {
    // if user clicked a node, store it as a path end
    pathEnds = [pathEnds[1], null];
    pathEnds[1] = id;

    // if we have 2 ends, give them to the selection change handler
    if (pathEnds[0] !== null) selectionChanged(pathEnds[0], pathEnds[1]);
  } else if (id === null || validatedItem) {
    // user clicked the background or a link
    resetSelection();
  }
}

function initialiseInteractions() {
  chart.on("click", selectNode);
  // prevent users selecting more than one node
  chart.on("selection-change", () => {
    if (chart.selection()[1]) chart.selection([]);
  });

  const radioButtons = document.querySelectorAll('input[name="option"]');
  Array.from(radioButtons).forEach((button) => {
    button.addEventListener("click", () => {
      pathWeighting = button.value;
      selectionChanged(pathEnds[0], pathEnds[1]);
    });
  });
}

async function startKeyLines() {
  const chartOptions = {
    logo: { u: "/images/Logo.png" },
    linkStyle: { inline: true },
    drag: { links: false },
    handMode: true,
    overview: false,
    minZoom: 0.45,
    selectedLink: {
      border: {
        radius: 3,
        colour: colours.themeDark,
        width: 1,
      },
      fbc: colours.themeBase,
      fc: "white",
      c: colours.themeDark,
    },
    selectedNode: {
      bw: 4,
      fc: "white",
    },
    zoom: {
      adaptiveStyling: false,
    },
  };

  chart = await KeyLines.create({
    container: "klchart",
    options: chartOptions,
  });

  chart.load(data());
  chart.zoom("fit", { animate: false });
  initialiseInteractions();
}

window.addEventListener("DOMContentLoaded", startKeyLines);
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Find Shortest Paths</title>
    <link rel="stylesheet" type="text/css" href="/css/keylines.css">
    <link rel="stylesheet" type="text/css" href="/css/minimalsdk.css">
    <link rel="stylesheet" type="text/css" href="/css/sdk-layout.css">
    <link rel="stylesheet" type="text/css" href="/css/demo.css">
    <script src="/vendor/jquery.js" defer type="text/javascript"></script>
    <script id="keylines" src="/public/keylines.umd.cjs" defer type="text/javascript"></script>
    <script src="/findpath.js" crossorigin="use-credentials" type="module"></script>
  </head>
  <body>
    <div class="chart-wrapper demo-cols">
      <div class="tab-content-panel flex1" id="lhs" data-tab-group="rhs">
        <div class="toggle-content is-visible" id="lhsVisual" style="width:100%; height: 100%;">
          <div class="klchart" id="klchart">
          </div>
        </div>
      </div>
      <div class="rhs citext closed" id="demorhscontent">
        <div class="title-bar">
          <svg viewBox="0 0 360 60" style="max-width: 360px; max-height: 60px; font-size: 24px; font-family: Raleway; flex: 1;">
            <text x="0" y="38" style="width: 100%;">Find Shortest Paths</text>
          </svg>
        </div>
        <div class="tab-content-panel" data-tab-group="rhs">
          <div class="toggle-content is-visible tabcontent" id="controlsTab">
            <form autocomplete="off" onsubmit="return false" id="rhsForm">
              <div class="cicontent">
                <p>Click on two nodes to find the shortest path between them.</p>
                <div class="form-stacked pull-left text-left" style="margin-left:15px;margin-top:20px;">
                  <fieldset>
                    <legend>Select Weight</legend>
                    <label class="radio">
                      <input type="radio" name="option" value="time" checked="checked" metric="Hr">Time
                    </label>
                    <label class="radio">
                      <input type="radio" name="option" value="distance" metric="km">Distance
                    </label>
                    <label class="radio">
                      <input type="radio" name="option" value="equal" metric="">None
                    </label>
                  </fieldset>
                </div>
                <table class="table">
                  <thead>
                    <tr>
                      <th colspan="2" id="title">Path Description</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr>
                      <td>From:</td>
                      <td><strong class="info" id="startNode"></strong></td>
                    </tr>
                    <tr>
                      <td>To:</td>
                      <td><strong class="info" id="endNode"></strong></td>
                    </tr>
                  </tbody>
                  <thead>
                    <tr>
                      <th class="info" colspan="2" style="color: red;" id="pathInfoTitle">&nbsp;</th>
                    </tr>
                  </thead>
                  <tbody>
                    <tr class="pathData">
                      <td>Number of Links:</td>
                      <td><strong class="info" id="links"></strong></td>
                    </tr>
                    <tr class="pathData">
                      <td>Weight Value:</td>
                      <td><strong class="info" id="weight"></strong></td>
                    </tr>
                  </tbody>
                </table>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source