Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,22 @@ NetJSON format used internally is based on [networkgraph](http://netjson.org/rfc

You can customize the style of GeoJSON features using `style` property. The list of all available properties can be found in the [Leaflet documentation](https://leafletjs.com/reference.html#geojson).

- `bookmarkableActions`

Configuration for adding url fragments when a node is clicked.

```JS
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be:

Suggested change
```JS
```javascript

bookmarkableActions:{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bookmarkableActions:{
bookmarkableActions: {

enabled: boolean,
id: string
}
```

You can enable or disable adding url fragments by setting enabled to true or false. When enabled, the following parameters are added to the URL:
1. id – A prefix used to uniquely identify the map.
2. nodeId – The id of the selected node.
When a URL containing these fragments is opened, the click event associated with the given nodeId is triggered, and if the map is based on Leaflet, the view will automatically center on that node.

- `onInit`

The callback function executed on initialization of `NetJSONGraph` instance.
Expand Down
5 changes: 5 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ <h1>NetJSONGraph.js Example Demos</h1>
>Indoor map</a
>
</div>
<div class="cards">
<a href="./examples/netjsonmap-indoormap-overlay.html" target="_blank"
>Indoor map as Ovelay of Geographic map</a
>
</div>
<div class="cards">
<a href="./examples/netjsonmap-plugins.html" target="_blank"
>Leaflet plugins</a
Expand Down
8 changes: 8 additions & 0 deletions public/example_templates/netjsongraph.html
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
const graph = new NetJSONGraph("../assets/data/netjsonmap.json", {
//control label visibility by graph zoom (continuous scale)
showGraphLabelsAtZoom: 2,
hashParams: {
show: true,
type: "basicUsage",
},
linkCategories: [
{
name: "down",
Expand All @@ -84,6 +88,10 @@
},
},
],
bookmarkableActions: {
enabled: true,
id: "basicUsage",
},
prepareData: (data) => {
data.links.map((link) => {
link.properties = link.properties || {};
Expand Down
201 changes: 201 additions & 0 deletions public/example_templates/netjsonmap-indoormap-overlay.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
<!doctype html>
<html lang="en">
<head>
<title>Network Map Visualization</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css"
integrity="sha512-hoalWLoI8r4UszCkZ5kL8vayOGVae1oxXe/2A4AO6J9+580uKHDO3JdHb7NzwwzK5xr/Fs0W40kiNHxM9vyTtQ=="
crossorigin=""
/>
<!-- theme can be easily customized via css -->
<link href="../lib/css/netjsongraph-theme.css" rel="stylesheet" />
<link href="../lib/css/netjsongraph.css" rel="stylesheet" />
<style>
body{
margin: 0;
padding: 0;
}
#map-container {
position: relative;
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
#map-content {
width: 100%;
height: 100%;
}
#indoormap-container {
position: absolute; /* overlay */
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
height: 90%;
padding: 20px;
border-radius: 8px;
align-items: center;
justify-content: center;
z-index: 9999;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
border-radius: 7px;
background-color: white;
}
#indoormap-close {
position: absolute;
top: 2px;
right: 5px;
color: gray;
border: none;
padding: 2px, 5px;
cursor: pointer;
border-radius: 4px;
font-size: 18px;
z-index: 9999;
}
#netjson-indoormap {
width: 100%;
height: 100%;
flex: 1;
}
#indoormap-container .njg-container .sideBarHandle {
top: 35px;
left: 390px
}
#indoormap-container .njg-container .hidden .sideBarHandle {
left: 35px;
}
</style>
</head>
<body>
<div id="map-container">
<div id="map-content"></div>
</div>

<script>
const netjsonmap = new NetJSONGraph("../assets/data/netjsonmap.json", {
el: "#map-content",
render: "map",
mapOptions: {
center: [46.86764405052012, 19.675998687744144],
zoom: 5,
nodeConfig: {
label: {
show: false,
offset: [0, -10],
},
},
baseOptions: {media: [{option: {tooltip: {show: true}}}]},
},
bookmarkableActions: {
enabled: true,
id: "geoMap",
},
prepareData: (data) => {
data.nodes.forEach((n) => {
n.label = n.name;
n.properties = n.properties || {};
n.properties.location = n.location;
});
data.links.forEach((l) => {
l.properties = l.properties || {};
l.category = l.properties.status || "up";
});
return data;
},
onClickElement(type, data) {
openIndoorMap();
},
});
netjsonmap.render();

function createIndoorMapContainer() {
const container = document.createElement("div");
container.id = "indoormap-container";

const closeBtn = document.createElement("span");
closeBtn.id = "indoormap-close";
closeBtn.innerHTML = "&times;";

const netjsonDiv = document.createElement("div");
netjsonDiv.id = "netjson-indoormap";

container.appendChild(closeBtn);
container.appendChild(netjsonDiv);

return container;
}

function closeButtonHandler(container) {
const closeBtn = container.querySelector("#indoormap-close");
closeBtn.addEventListener("click", () => {
container.remove();
});
}

function openIndoorMap() {
const indoorMapContainer = createIndoorMapContainer();
closeButtonHandler(indoorMapContainer);
const mapContainer = document.getElementById("map-container");
mapContainer.appendChild(indoorMapContainer);

const indoor = new NetJSONGraph(
"../assets/data/netjsonmap-indoormap.json",
{
el: "#netjson-indoormap",
render: "map",
crs: L.CRS.Simple,
mapOptions: {
center: [50, 50],
zoom: 1,
minZoom: -2,
maxZoom: 2,
nodeConfig: {
label: {
show: false,
},
animation: false,
},
baseOptions: {media: [{option: {tooltip: {show: true}}}]},
},
bookmarkableActions:{
enabled: true,
id: "indoorMap",
},
prepareData: (data) => {
data.nodes.forEach((n) => {
n.label = n.name;
n.properties = n.properties || {};
n.properties.location = n.location;
});
},
onReady: async function () {
const map = this.leaflet;
map.eachLayer((layer) => layer._url && map.removeLayer(layer));
const img = new Image();
imageUrl = "../assets/images/floorplan.png";
img.src = imageUrl;
await img.decode()
const aspectRatio = img.width / img.height;
const h = 700;
const w = aspectRatio * h;
const zoom = map.getMaxZoom() - 1;
const sw = map.unproject([0, h * 2], zoom);
const ne = map.unproject([w * 2, 0], zoom);
const bnds = new L.LatLngBounds(sw, ne);
L.imageOverlay(imageUrl, bnds).addTo(map);
map.fitBounds(bnds);
map.setMaxBounds(bnds);
},
},
);
indoor.render();
}
</script>
</body>
</html>
30 changes: 16 additions & 14 deletions public/example_templates/netjsonmap-indoormap.html
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@
],
},
},
bookmarkableActions: {
enabled: true,
id: "indoorMap"
},

// Convert to internal json format
prepareData: function (data) {
Expand All @@ -117,25 +121,23 @@
});
},

onReady: function presentIndoormap() {
onReady: async function presentIndoormap() {
const netjsonmap = this.leaflet;
const image = new Image();
const imageUrl = "../assets/images/floorplan.png";
image.src = imageUrl;
await image.decode()
const aspectRatio = image.width / image.height;
const h = 700;
const w = aspectRatio * h;
const zoom = netjsonmap.getMaxZoom() - 1;
const bottomLeft = netjsonmap.unproject([0, h * 2], zoom);
const upperRight = netjsonmap.unproject([w * 2, 0], zoom);
const bounds = new L.LatLngBounds(bottomLeft, upperRight);

image.onload = function () {
const aspectRatio = image.width / image.height;
const h = 700;
const w = aspectRatio * h;
const zoom = netjsonmap.getMaxZoom() - 1;
const bottomLeft = netjsonmap.unproject([0, h * 2], zoom);
const upperRight = netjsonmap.unproject([w * 2, 0], zoom);
const bounds = new L.LatLngBounds(bottomLeft, upperRight);

L.imageOverlay(imageUrl, bounds).addTo(netjsonmap);
netjsonmap.fitBounds(bounds);
netjsonmap.setMaxBounds(bounds);
};
L.imageOverlay(imageUrl, bounds).addTo(netjsonmap);
netjsonmap.fitBounds(bounds);
netjsonmap.setMaxBounds(bounds);

// Remove any default tile layers and show only the floorplan image.
netjsonmap.eachLayer((layer) => {
Expand Down
7 changes: 5 additions & 2 deletions public/example_templates/netjsonmap.html
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,10 @@
<script type="text/javascript">
const map = new NetJSONGraph("../assets/data/netjsonmap.json", {
render: "map",
// set map initial state.
bookmarkableActions: {
enabled: true,
id: "geographicMap"
},
mapOptions: {
center: [46.86764405052012, 19.675998687744144],
zoom: 5,
Expand All @@ -96,7 +99,7 @@
},
},
],
// Convert to internal json format

prepareData: (data) => {
data.nodes.map((node) => {
node.label = node.name;
Expand Down
4 changes: 4 additions & 0 deletions src/js/netjsongraph.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,10 @@ const NetJSONGraphDefaultConfig = {
},
],
linkCategories: [],
bookmarkableActions: {
enabled: false,
id: null,
},

/**
* @function
Expand Down
25 changes: 23 additions & 2 deletions src/js/netjsongraph.core.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,19 @@ class NetJSONGraph {
const [JSONParam, ...resParam] = this.JSONParam;

this.config.onRender.call(this);
this.event.once("onReady", this.config.onReady.bind(this));
// Ensure applyUrlFragmentState runs only after onReady has completed,
// as onReady may perform asynchronous operations
const onReadyDone = new Promise((resolve) => {
this.event.once("onReady", async () => {
await this.config.onReady.call(this);
resolve();
});
});
this.event.once("onLoad", this.config.onLoad.bind(this));
this.event.once("applyUrlFragmentState", async () => {
await onReadyDone;
this.utils.applyUrlFragmentState.call(this, this);
});
this.utils.paginatedDataParse
.call(this, JSONParam)
.then((JSONData) => {
Expand Down Expand Up @@ -100,9 +111,19 @@ class NetJSONGraph {
this.config.maxPointsFetched - 1,
JSONData.nodes.length - this.config.maxPointsFetched,
);
const nodeSet = new Set(JSONData.nodes.map((node) => node.id));
const nodeSet = new Set();
// Build a lookup map (this.nodeLinkIndex) for quick access to node or link data.
// Uses node IDs as keys for nodes and "source~target" as keys for links.
// This avoids repeated traversal when restoring state from URL fragments.
this.nodeLinkIndex = {};
JSONData.nodes.forEach((node) => {
nodeSet.add(node.id);
this.nodeLinkIndex[node.id] = node;
});
JSONData.links = JSONData.links.filter((link) => {
if (nodeSet.has(link.source) && nodeSet.has(link.target)) {
const key = `${link.source}~${link.target}`;
this.nodeLinkIndex[key] = link;
return true;
}
if (!nodeSet.has(link.source)) {
Expand Down
Loading
Loading