// Useful functions for our asset-specific Leaflet code
function addTileLayer(leafletMap, mapboxAccessToken) {
/*
Add the tile layer for FlexMeasures.
Configure tile size, Mapbox API access and attribution.
*/
var tileLayer = new L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
attribution: '© Mapbox © OpenStreetMap Improve this map',
tileSize: 512,
maxZoom: 18,
zoomOffset: -1,
id: 'mapbox/streets-v11',
accessToken: mapboxAccessToken
});
tileLayer.addTo(leafletMap);
// add link for Mapbox logo (logo added via CSS)
$("#" + leafletMap._container.id).append(
'Mapbox'
);
}
function clickPan(e, data) {
// set view such that the target asset lies slightly below the center of the map
targetLatLng = e.target.getLatLng()
targetZoom = assetMap.getZoom()
targetPoint = assetMap.project(targetLatLng, targetZoom).subtract([0, 50]),
targetLatLng = assetMap.unproject(targetPoint, targetZoom);
assetMap.setView(targetLatLng, targetZoom);
}
/**
* Injects a custom spiderfy layout function into the marker cluster, passing both
* child markers and the cluster center point to the layout function.
*
* @param {L.MarkerCluster} markerCluster - The marker cluster instance to modify.
*/
function passMarkersToSpiderfyShapePositions (markerCluster) {
const childMarkers = markerCluster.getAllChildMarkers();
const centerPt = markerCluster._group._map.latLngToLayerPoint(markerCluster._latlng);
const shapeFn = markerCluster._group?.options?.spiderfyShapePositionsWithMarkers;
if (typeof shapeFn === 'function') {
markerCluster._group.options.spiderfyShapePositions = () =>
shapeFn(childMarkers, centerPt);
}
}
/**
* Removes all spider legs (connecting lines) from the map for the given marker cluster.
*
* @param {L.MarkerCluster} markerCluster - The marker cluster from which to remove spider legs.
*/
function removeSpiderLegs(markerCluster) {
const map = markerCluster._group._map;
for (const marker of markerCluster.getAllChildMarkers()) {
if (marker._spiderLeg) {
map.removeLayer(marker._spiderLeg);
}
}
}
/**
* Computes a tree-based layout for markers, organizing them into a forest centered
* around a given point. Markers must have `id` and `parentId` options.
*
* @param {Array} markers - Array of markers with hierarchical relationships.
* @param {L.Point} centerPt - The center point to base the layout around.
* @param {number} [spacingX=50] - Horizontal spacing between sibling nodes.
* @param {number} [spacingY=100] - Vertical spacing between levels.
* @param {number} [treeSpacing=100] - Spacing between root trees.
* @returns {Array} Array of new positions (as layer points) for the markers.
*/
function computeCenteredTreeLayout(markers, centerPt, spacingX = 50, spacingY = 100, treeSpacing = 100) {
const nodeMap = new Map();
// Add virtual parent nodes if they are missing a marker (no lat/lng)
const allParentIds = new Set(markers.map(m => m.options.parentId).filter(id => id !== null));
for (const parentId of allParentIds) {
if (!nodeMap.has(parentId)) {
// Use the first child with this parent to determine location
const child = markers.find(m => m.options.parentId === parentId);
if (child) {
nodeMap.set(parentId, {
id: parentId,
parentId: null,
children: [],
x: 0,
y: 0,
virtual: true // you can tag it if needed
});
}
}
}
markers.forEach(marker => {
const { id, parentId } = marker.options;
nodeMap.set(id, { ...marker.options, children: [], x: 0, y: 0 });
});
let roots = [];
nodeMap.forEach(node => {
if (node.parentId === null) {
roots.push(node);
} else {
const parent = nodeMap.get(node.parentId);
if (parent) {
parent.children.push(node);
}
}
});
const positionMap = new Map(); // id -> { dx, dy, level }
let currentX = 0;
function layoutTree(node, depth) {
if (node.children.length === 0) {
node.x = currentX;
currentX += spacingX;
} else {
for (const child of node.children) {
layoutTree(child, depth + 1);
}
const xs = node.children.map(child => child.x);
node.x = (Math.min(...xs) + Math.max(...xs)) / 2;
}
node.y = depth * spacingY;
positionMap.set(node.id, { dx: node.x, dy: node.y, level: depth });
}
let globalXOffset = 0;
for (const root of roots) {
const startX = currentX;
layoutTree(root, 0);
// Adjust all dx in this tree by (startX - minX)
const treeNodes = [...nodeMap.values()].filter(n => positionMap.has(n.id));
const minX = Math.min(...treeNodes.map(n => positionMap.get(n.id).dx));
const offset = startX - minX;
for (const n of treeNodes) {
const pos = positionMap.get(n.id);
pos.dx += offset;
}
// Update currentX for next tree
const maxX = Math.max(...treeNodes.map(n => positionMap.get(n.id).dx));
currentX = maxX + treeSpacing;
}
// === Center the forest around (0, 0) ===
const allPositions = Array.from(positionMap.values());
const minDx = Math.min(...allPositions.map(p => p.dx));
const maxDx = Math.max(...allPositions.map(p => p.dx));
const centerX = (minDx + maxDx) / 2;
const maxLevel = Math.max(...allPositions.map(p => p.level));
const centerLevel = maxLevel / 2;
const centerY = centerLevel * spacingY;
for (const pos of allPositions) {
pos.dx -= centerX;
pos.dy -= centerY;
}
// Final result in original order
return markers.map(marker => {
const { dx, dy } = positionMap.get(marker.options.id);
// console.log(marker.options.id, ": ", dx, ", ", dy);
return L.point(
{
x: centerPt.x + dx,
y: centerPt.y + dy
}
);
});
}
/**
* Performs a tree-style animated spiderfy for a cluster, using parent-child relationships
* between markers to create animated spider legs from parent to child.
*
* @param {Array} childMarkers - The markers to be spiderfied.
* @param {Array} positions - The target layer point positions for the markers.
*/
function _animationTreeSpiderfy(childMarkers, positions) {
var me = this,
group = this._group,
map = group._map,
fg = group._featureGroup,
thisLayerLatLng = this._latlng,
thisLayerPos = map.latLngToLayerPoint(thisLayerLatLng),
svg = L.Path.SVG,
legOptions = L.extend({}, this._group.options.spiderLegPolylineOptions), // Copy the options so that we can modify them for animation.
finalLegOpacity = legOptions.opacity,
i, m, leg, legPath, legLength, newPos;
if (finalLegOpacity === undefined) {
finalLegOpacity = L.MarkerClusterGroup.prototype.options.spiderLegPolylineOptions.opacity;
}
if (svg) {
// If the initial opacity of the spider leg is not 0 then it appears before the animation starts.
legOptions.opacity = 0;
// Add the class for CSS transitions.
legOptions.className = (legOptions.className || '') + ' leaflet-cluster-spider-leg';
} else {
// Make sure we have a defined opacity.
// legOptions.opacity = finalLegOpacity;
legOptions.opacity = 0; // Seita monkeypatch
}
group._ignoreMove = true;
// Seita monkeypatch
// Build a quick lookup map by marker id
const markerMap = new Map(childMarkers.map(m => [m.options.id, m]));
// Add markers and spider legs to map, hidden at our center point.
// Traverse in ascending order to make sure that inner circleMarkers are on top of further legs. Normal markers are re-ordered by newPosition.
// The reverse order trick no longer improves performance on modern browsers.
for (i = 0; i < childMarkers.length; i++) {
m = childMarkers[i];
childPos = positions[i]
// Seita monkeypatch
const parentId = m.options.parentId;
var hasParentPos = true;
if (parentId == null) {
hasParentPos = false;
};
const parentMarker = markerMap.get(parentId);
if (!parentMarker) {
hasParentPos = false;
};
const parentPos = positions[childMarkers.indexOf(parentMarker)];
if (!parentPos) {
hasParentPos = false;
};
newPos = map.layerPointToLatLng(positions[i]);
if (hasParentPos == true) {
oldPos = map.layerPointToLatLng(parentPos);
} else {
oldPos = newPos;
}
// Add the leg before the marker, so that in case the latter is a circleMarker, the leg is behind it.
leg = new L.Polyline([oldPos, newPos], legOptions);
// leg = new L.Polyline([thisLayerLatLng, newPos], legOptions);
map.addLayer(leg);
m._spiderLeg = leg;
// Explanations: https://jakearchibald.com/2013/animated-line-drawing-svg/
// In our case the transition property is declared in the CSS file.
if (svg) {
legPath = leg._path;
legLength = legPath.getTotalLength() + 0.1; // Need a small extra length to avoid remaining dot in Firefox.
legPath.style.strokeDasharray = legLength; // Just 1 length is enough, it will be duplicated.
legPath.style.strokeDashoffset = legLength;
}
}
for (i = 0; i < childMarkers.length; i++) {
m = childMarkers[i];
// If it is a marker, add it now and we'll animate it out
if (m.setZIndexOffset) {
m.setZIndexOffset(1000000); // Make normal markers appear on top of EVERYTHING
}
if (m.clusterHide) {
m.clusterHide();
}
// Vectors just get immediately added
fg.addLayer(m);
if (m._setPos) {
m._setPos(thisLayerPos);
}
}
group._forceLayout();
group._animationStart();
// Reveal markers and spider legs.
for (i = childMarkers.length - 1; i >= 0; i--) {
newPos = map.layerPointToLatLng(positions[i]);
m = childMarkers[i];
//Move marker to new position
m._preSpiderfyLatlng = m._latlng;
m.setLatLng(newPos);
if (m.clusterShow) {
m.clusterShow();
}
// Animate leg (animation is actually delegated to CSS transition).
if (svg) {
leg = m._spiderLeg;
legPath = leg._path;
legPath.style.strokeDashoffset = 0;
//legPath.style.strokeOpacity = finalLegOpacity;
leg.setStyle({opacity: finalLegOpacity});
}
}
this.setOpacity(0.3);
group._ignoreMove = false;
setTimeout(function () {
group._animationEnd();
group.fire('spiderfied', {
cluster: me,
markers: childMarkers
});
// Seita monkeypatch
for (i = childMarkers.length - 1; i >= 0; i--) {
m = childMarkers[i];
leg = m._spiderLeg;
leg.setStyle({opacity: finalLegOpacity});
}
}, 200);
}