//
// 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
