//
//  Copyright © 2011-2025 Cambridge Intelligence Limited.
//  All rights reserved.
//
//  Sample Code
//
//! Create floorplans or projections with images and CRS.
import KeyLines from "keylines";

import {
  data,
  styleCombo,
  styleComboLink,
  drilldownOffsets,
  siteLocations,
} from "./imagemaps-data.js";

let chart;

// Metadata to control the CRS image layer
const images = {
  World: {
    url: "/images/imagemaps/wiki-simple-world-map.svg",
    width: 2000,
    height: 1075,
  },
  "New York": {
    url: "/images/imagemaps/factory1.jpg",
    width: 2058,
    height: 1050,
  },
  Bangalore: {
    url: "/images/imagemaps/factory2.jpg",
    width: 2058,
    height: 1050,
  },
  "Central Server": {
    url: "/images/imagemaps/webserver.jpg",
    width: 2058,
    height: 1050,
  },
};

// Track combos we've created by location
const comboMap = {};

let currentBackground;
let backgroundOverlay;

// Update the background CRS image and transition nicely
function updateCRSImage(imageId, animate) {
  const prevOverlay = backgroundOverlay;
  const data = images[imageId];
  const bounds = [
    [0, 0],
    [data.height, data.width],
  ];
  const maxOpacity = 0.6; // Wash out the image colours a little bit

  // Set the background CRS image
  const overlayOpts = { opacity: animate ? 0 : maxOpacity };
  backgroundOverlay = L.imageOverlay(data.url, bounds, overlayOpts);

  if (animate) {
    backgroundOverlay.on("load", () => {
      // Use JQuery to fade the background
      $(backgroundOverlay.getElement()).animate(
        { opacity: maxOpacity },
        300,
        "swing",
        () => {
          prevOverlay.remove();
        }
      );
    });
  }

  backgroundOverlay.addTo(chart.map().leafletMap());
}

function toggleButtons(enable) {
  const buttons = document.querySelectorAll('input[type="button"]');
  for (let i = 0; i < buttons.length; i++) {
    const button = buttons[i];
    const disable = currentBackground === button.value || !enable;
    if (disable) {
      button.setAttribute("disabled", "true");
      button.classList.add("disabled");
    } else {
      button.removeAttribute("disabled");
      button.classList.remove("disabled");
    }
  }
}

// Uncombine the combo representing the current background
function uncombineBackground() {
  const uncombineOpts = { animate: true, time: 600, select: false };
  const activeCombo = comboMap[currentBackground];
  delete comboMap[currentBackground];
  return chart.combo().uncombine(activeCombo, uncombineOpts);
}

function updateLinkStyles() {
  const updates = [];

  chart.each({ type: "link", items: "toplevel" }, (link) => {
    if (chart.combo().isCombo(link.id)) {
      const links = chart.combo().info(link.id).links;
      let isAlert;
      links.forEach((l) => {
        if (l.d.alert) {
          isAlert = true;
        }
      });
      // Style rules are defined in imagemaps-data.js
      updates.push(styleComboLink(link.id, isAlert));
    }
  });
  return chart.setProperties(updates, false);
}

// Figure out which combo nodes we need for the current background, and create them
async function createCombos(animate) {
  const places = {};
  chart.each({ type: "node", items: "underlying" }, (node) => {
    if (!comboMap[node.d.location]) {
      if (!places[node.d.location]) {
        places[node.d.location] = [];
      }
      places[node.d.location].push(node.id);
    }
  });

  const combos = [];
  Object.keys(places).forEach((p) => {
    if (p !== currentBackground) {
      combos.push({
        ids: places[p],
        label: null,
        glyph: null,
        // Style rules are defined in imagemaps-data.js
        style: styleCombo(p),
      });
    }
  });

  const comboIds = await chart
    .combo()
    .combine(combos, { animate, select: false });
  // Update the combo map with the new ids
  comboIds.forEach((cid) => {
    const combo = chart.getItem(cid);
    comboMap[combo.d.location] = cid;
  });
}

function updateComboPositions(animate) {
  const updates = [];
  const offsets = drilldownOffsets[currentBackground] || {};

  // Position nodes according to the global or local image map
  const positions = currentBackground === "World" ? siteLocations : offsets;

  chart.each({ items: "toplevel" }, (item) => {
    if (item.type === "node") {
      if (positions[item.d.location]) {
        updates.push({
          id: item.id,
          pos: positions[item.d.location],
        });
      }
    } else if (item.type === "link") {
      // Hide links between combos while drilled-down into a site
      if (chart.combo().isCombo(item.id1) && chart.combo().isCombo(item.id2)) {
        updates.push({
          id: item.id,
          hi: currentBackground !== "World",
        });
      }
    }
  });
  return chart.animateProperties(updates, {
    time: animate ? 400 : 1,
  });
}

async function setBackground(id) {
  if (id !== currentBackground) {
    toggleButtons(false);
    const allowAnimation = !!currentBackground;
    currentBackground = id;

    updateCRSImage(id, allowAnimation);
    await createCombos(allowAnimation);
    await updateComboPositions(allowAnimation);
    await uncombineBackground();
    await updateLinkStyles();
    await chart.zoom("fit", { animate: allowAnimation });

    toggleButtons(true);
  }
}

async function klReady(loadedChart) {
  chart = loadedChart;

  const buttons = document.querySelectorAll('input[type="button"]');
  for (let i = 0; i < buttons.length; i++) {
    buttons[i].onclick = (e) => {
      setBackground(e.target.value);
    };
  }

  chart.on("double-click", (e) => {
    const item = chart.getItem(e.id);
    if (item?.type === "node") {
      if (images[item.d.location]) {
        setBackground(item.d.location);
      }
    } else {
      setBackground("World");
    }
    e.preventDefault();
  });

  await chart.map().options({
    leaflet: {
      crs: L.CRS.Simple,
      minZoom: -1.9,
      maxZoom: 1,
      zoomSnap: 0.5,
    },
    tiles: null,
    animate: false,
    padding: 70,
  });
  await chart.load(data);
  await chart.map().show();
  await chart.map().leafletMap().doubleClickZoom.disable();

  return setBackground("World");
}

async function startKeyLines() {
  const imageAlignment = {
    "fas fa-cloud": { e: 0.9 },
    "fas fa-industry": { e: 0.9 },
    "fas fa-server": { e: 0.9 },
    "fas fa-plus": { e: 1.1 },
    "fas fa-laptop": { e: 0.8 },
  };

  const chartOptions = {
    logo: "/images/Logo.png",
    backColour: "white",
    iconFontFamily: "Font Awesome 5 Free",
    selectedNode: {
      ha0: {
        c: "rgba(197, 150, 247, 0.7)",
        r: 40,
        w: 10,
      },
    },
    selectedLink: {
      c: "rgba(197, 150, 247, 0.7)",
    },
    imageAlignment,
    linkEnds: { avoidLabels: false },
  };

  const loadedChart = await KeyLines.create({
    container: "klchart",
    type: "chart",
    options: chartOptions,
  });
  return klReady(loadedChart);
}

document.addEventListener("DOMContentLoaded", () => {
  document.fonts.load('24px "Font Awesome 5 Free"').then(startKeyLines);
});
<!DOCTYPE html>
<html lang="en" style="background-color: #2d383f;">
  <head>
    <meta charset="UTF-8">
    <title>Using Images with Leaflet</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">
    <link rel="stylesheet" type="text/css" href="/css/leaflet.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/fontawesome.css">
    <link rel="stylesheet" type="text/css" href="/css/fontawesome5/solid.css">
    <style>
      .fas.fa-plus {
        color: white;
        background-color: black;
        border-radius: 8px;
        font-size: 12px;
        padding: 2px 3px;
        margin: 0px 2px;
      }
      
    </style>
    <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="/vendor/leaflet.js" defer type="text/javascript"></script>
    <script src="/imagemaps.js" defer 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%;">Using Images with Leaflet</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>
                  Double-click sites or use the buttons below to better understand connections
                  between machines on a private network and the public internet.
                </p>
                <p style="margin: 0 16px;">
                  <input class="btn btn-block disabled" type="button" value="World" style="text-align: center;">
                  <input class="btn btn-block disabled" type="button" value="New York" style="text-align: center;">
                  <input class="btn btn-block disabled" type="button" value="Bangalore" style="text-align: center;">
                  <input class="btn btn-block disabled" type="button" value="Central Server" style="text-align: center;">
                </p>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
    <div id="moreParent">
      <div id="moreContainer">
      </div>
    </div>
    <div id="lazyScripts">
    </div>
  </body>
</html>
Loading source