Skip to content

Commit 0ef49db

Browse files
committed
[fix] Update indoor map example and bookmarkableActions #397
Fixed CRS conflict in indoor map example and added a new parameter 'zoomLevel' in bookmarkableActions. Added support for link interactions, fix tests and update docs. Fixes #397
1 parent 9d19d07 commit 0ef49db

File tree

9 files changed

+177
-140
lines changed

9 files changed

+177
-140
lines changed

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -469,17 +469,32 @@ NetJSON format used internally is based on [networkgraph](http://netjson.org/rfc
469469

470470
Configuration for adding url fragments when a node is clicked.
471471

472-
```JS
473-
bookmarkableActions:{
472+
```javascript
473+
bookmarkableActions: {
474474
enabled: boolean,
475-
id: string
475+
id: string,
476+
zoomLevel: number
476477
}
477478
```
478479

480+
**Note: This feature is disabled by default.**
481+
479482
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:
480483
1. id – A prefix used to uniquely identify the map.
481484
2. nodeId – The id of the selected node.
482-
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.
485+
3. zoomLevel – The zoom level applied when restoring the state from the URL fragments.
486+
487+
This feature allows you to create shareable and restorable map or graph states using URL fragments. When this feature is enabled, the URL updates automatically whenever you click a node or a link in your NetJSONGraph visualization. This makes it easy to share a specific view, restore it later, or navigate between different states using the browser’s back and forward buttons.
488+
489+
When you click on a node or link, information is added to the URL as a fragment (for example for node: `#map1-node=device-123` and for link: `#map1-link=deviceA~deviceB`). If you open such a URL directly in your browser, the visualization will automatically restore that exact state triggering the click event on the corresponding node or link and centering the map or graph accordingly.
490+
491+
This feature works across all ECharts graphs, as well as Leaflet-based maps including geographic and indoor floorplan maps and it supports multiple maps or graphs on the same page. The id parameter is used to uniquely identify which visualization the URL fragment belongs to (for eample: `#map1-node=device-1;#map2-node=device-2` ).
492+
493+
For nodes, the behavior depends on the type of visualization in Leaflet maps, clicking a node updates the URL and on apllying the state from url it automatically centers the map on that node, in addition to triggering its click event. In ECharts graphs, only triggers the click event for the node.
494+
495+
For links, the URL fragment uses the format `source~target` as the `nodeId`. Opening such a URL restores the initial map or graph view and triggers the corresponding link click event.
496+
497+
If you need to manually remove the URL fragment, you can call the built-in utility method: `netjsongraphInstance.utils.removeUrlFragment('id');` where id is `bookmarkableActions.id`.
483498

484499
- `onInit`
485500

public/example_templates/netjsongraph.html

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@
6969
const graph = new NetJSONGraph("../assets/data/netjsonmap.json", {
7070
//control label visibility by graph zoom (continuous scale)
7171
showGraphLabelsAtZoom: 2,
72-
hashParams: {
73-
show: true,
74-
type: "basicUsage",
75-
},
7672
linkCategories: [
7773
{
7874
name: "down",

public/example_templates/netjsonmap-indoormap-overlay.html

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@
6363
height: 100%;
6464
flex: 1;
6565
}
66+
#netjson-indoormap .njg-sideBar {
67+
background: #f4f4f4;
68+
border-right: 1px solid rgba(0, 0, 0, 0.2);
69+
}
6670
#indoormap-container .njg-container .sideBarHandle {
6771
top: 35px;
6872
left: 390px
@@ -95,6 +99,7 @@
9599
bookmarkableActions: {
96100
enabled: true,
97101
id: "geoMap",
102+
zoomLevel: 10,
98103
},
99104
prepareData: (data) => {
100105
data.nodes.forEach((n) => {
@@ -149,12 +154,13 @@
149154
{
150155
el: "#netjson-indoormap",
151156
render: "map",
152-
crs: L.CRS.Simple,
153157
mapOptions: {
154-
center: [50, 50],
155-
zoom: 1,
156-
minZoom: -2,
157-
maxZoom: 2,
158+
center: [0, 0],
159+
zoom: 0,
160+
minZoom: 6,
161+
maxZoom: 10,
162+
zoomSnap: 0.5,
163+
zoomDelta: 0.5,
158164
nodeConfig: {
159165
label: {
160166
show: false,
@@ -163,7 +169,7 @@
163169
},
164170
baseOptions: {media: [{option: {tooltip: {show: true}}}]},
165171
},
166-
bookmarkableActions:{
172+
bookmarkableActions: {
167173
enabled: true,
168174
id: "indoorMap",
169175
},
@@ -176,26 +182,68 @@
176182
},
177183
onReady: async function () {
178184
const map = this.leaflet;
185+
// Remove default tile layers
179186
map.eachLayer((layer) => layer._url && map.removeLayer(layer));
187+
180188
const img = new Image();
181189
imageUrl = "../assets/images/floorplan.png";
182190
img.src = imageUrl;
183-
await img.decode()
191+
await img.decode();
192+
184193
const aspectRatio = img.width / img.height;
185194
const h = 700;
186-
const w = aspectRatio * h;
195+
const w = h * aspectRatio;
196+
187197
const zoom = map.getMaxZoom() - 1;
188-
const sw = map.unproject([0, h * 2], zoom);
189-
const ne = map.unproject([w * 2, 0], zoom);
190-
const bnds = new L.LatLngBounds(sw, ne);
198+
199+
const anchorLatLng = L.latLng(0, 0);
200+
const anchorPoint = map.project(anchorLatLng, zoom);
201+
const topLeft = L.point(anchorPoint.x - w / 2, anchorPoint.y - h / 2);
202+
const bottomRight = L.point(anchorPoint.x + w / 2, anchorPoint.y + h / 2);
203+
204+
const mapOptions = this.echarts.getOption();
205+
// Refer netjsonmap-indoormap.html for full explanation of this workaround
206+
mapOptions.series[0].data.forEach((data) => {
207+
const node = data.node;
208+
const px = Number(node.location.lng);
209+
const py = -Number(node.location.lat);
210+
const nodeProjected = L.point(topLeft.x + px, topLeft.y + py);
211+
const nodeLatLng = map.unproject(nodeProjected, zoom);
212+
213+
node.location = nodeLatLng
214+
node.properties.location = nodeLatLng;
215+
data.value = [nodeLatLng.lng, nodeLatLng.lat];
216+
});
217+
mapOptions.series[1].data.forEach((data) => {
218+
const soruce = data.coords[0];
219+
const sourcePx = Number(soruce[0]);
220+
const sourcePy = -Number(soruce[1]);
221+
const sourceNodeProjected = L.point(topLeft.x + sourcePx, topLeft.y + sourcePy);
222+
const sourceNodeLatLng = map.unproject(sourceNodeProjected, zoom);
223+
224+
const target = data.coords[1];
225+
const targetPx = Number(target[0]);
226+
const targetPy = -Number(target[1]);
227+
const targetNodeProjected = L.point(topLeft.x + targetPx, topLeft.y + targetPy);
228+
const targetNodeLatLng = map.unproject(targetNodeProjected, zoom);
229+
230+
data.coords[0] = [sourceNodeLatLng.lng, sourceNodeLatLng.lat];
231+
data.coords[1] = [targetNodeLatLng.lng, targetNodeLatLng.lat];
232+
});
233+
this.echarts.setOption(mapOptions);
234+
const nw = map.unproject(topLeft, zoom);
235+
const se = map.unproject(bottomRight, zoom);
236+
const bnds = L.latLngBounds(nw, se);
237+
191238
L.imageOverlay(imageUrl, bnds).addTo(map);
192239
map.fitBounds(bnds);
193240
map.setMaxBounds(bnds);
241+
map.invalidateSize();
194242
},
195243
},
196244
);
197245
indoor.render();
198246
}
199247
</script>
200248
</body>
201-
</html>
249+
</html>

public/example_templates/netjsonmap-indoormap.html

Lines changed: 83 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -45,72 +45,26 @@
4545
"../assets/data/netjsonmap-indoormap.json",
4646
{
4747
render: "map",
48-
crs: L.CRS.Simple,
4948
// set map initial state.
5049
mapOptions: {
51-
center: [48.577, 18.539],
50+
center: [0, 0],
5251
zoom: 0,
53-
zoomSnap: 0.3,
54-
minZoom: -1,
55-
maxZoom: 2,
52+
minZoom: 6,
53+
maxZoom: 10,
54+
zoomSnap: 0.5,
55+
zoomDelta: 0.5,
5656
nodeConfig: {
5757
label: {
5858
show: false,
5959
},
6060
animation: false,
6161
},
62-
linkConfig: {
63-
linkStyle: {
64-
width: 4,
65-
},
66-
animation: false,
67-
},
68-
baseOptions: {
69-
media: [
70-
{
71-
query: {
72-
minWidth: 320,
73-
maxWidth: 850,
74-
},
75-
option: {
76-
tooltip: {
77-
show: false,
78-
},
79-
},
80-
},
81-
{
82-
query: {
83-
minWidth: 851,
84-
},
85-
option: {
86-
tooltip: {
87-
show: true,
88-
},
89-
},
90-
},
91-
{
92-
query: {
93-
minWidth: 320,
94-
maxWidth: 400,
95-
},
96-
option: {
97-
series: [
98-
{
99-
label: {
100-
fontSize: "12px",
101-
},
102-
},
103-
],
104-
},
105-
},
106-
],
107-
},
62+
baseOptions: {media: [{option: {tooltip: {show: true}}}]},
10863
},
10964
bookmarkableActions: {
11065
enabled: true,
111-
id: "indoorMap"
66+
id: "indoorMap",
11267
},
113-
11468
// Convert to internal json format
11569
prepareData: function (data) {
11670
data.nodes.map((node) => {
@@ -121,34 +75,85 @@
12175
});
12276
},
12377

124-
onReady: async function presentIndoormap() {
125-
const netjsonmap = this.leaflet;
126-
const image = new Image();
127-
const imageUrl = "../assets/images/floorplan.png";
128-
image.src = imageUrl;
129-
await image.decode()
130-
const aspectRatio = image.width / image.height;
131-
const h = 700;
132-
const w = aspectRatio * h;
133-
const zoom = netjsonmap.getMaxZoom() - 1;
134-
const bottomLeft = netjsonmap.unproject([0, h * 2], zoom);
135-
const upperRight = netjsonmap.unproject([w * 2, 0], zoom);
136-
const bounds = new L.LatLngBounds(bottomLeft, upperRight);
78+
onReady: async function () {
79+
const map = this.leaflet;
80+
// Remove default tile layers
81+
map.eachLayer((layer) => layer._url && map.removeLayer(layer));
13782

138-
L.imageOverlay(imageUrl, bounds).addTo(netjsonmap);
139-
netjsonmap.fitBounds(bounds);
140-
netjsonmap.setMaxBounds(bounds);
83+
const img = new Image();
84+
imageUrl = "../assets/images/floorplan.png";
85+
img.src = imageUrl;
86+
await img.decode();
14187

142-
// Remove any default tile layers and show only the floorplan image.
143-
netjsonmap.eachLayer((layer) => {
144-
if (layer._url) {
145-
netjsonmap.removeLayer(layer);
146-
}
147-
});
148-
},
149-
},
150-
);
88+
const aspectRatio = img.width / img.height;
89+
const h = 700;
90+
const w = h * aspectRatio;
91+
92+
const zoom = map.getMaxZoom() - 1;
93+
94+
// Workaround for https://github.com/openwisp/netjsongraph.js/issues/397
95+
// To make the image center in the map at (0,0) coordinates
96+
const anchorLatLng = L.latLng(0, 0);
97+
const anchorPoint = map.project(anchorLatLng, zoom);
15198

99+
// Calculate the bounds of the image, with respect to the anchor point (0, 0)
100+
// Leaflet's pixel coordinates increase to the right and downwards
101+
// Unlike cartesian system where y increases upwards
102+
// So top-left will have negative y and bottom-right will have positive y
103+
// Similarly left will have negative x and right will have positive x
104+
const topLeft = L.point(anchorPoint.x - w / 2, anchorPoint.y - h / 2);
105+
const bottomRight = L.point(anchorPoint.x + w / 2, anchorPoint.y + h / 2);
106+
107+
// Update node coordinates to fit the image overlay
108+
// We get the node coordinates from the API in the format for L.CRS.Simple
109+
// So the coordinates is in for cartesian system with origin at top left corner
110+
// Rendering image in the third quadrant with topLeft as (0,0) and bottomRight as (w,-h)
111+
// So we convert py to positive and then project the point to get the corresponding topLeft
112+
// Then unproject the point to get the corresponding latlng on the map
113+
const mapOptions = this.echarts.getOption();
114+
mapOptions.series[0].data.forEach((data) => {
115+
const node = data.node;
116+
const px = Number(node.location.lng);
117+
const py = -Number(node.location.lat);
118+
const nodeProjected = L.point(topLeft.x + px, topLeft.y + py);
119+
const nodeLatLng = map.unproject(nodeProjected, zoom);
120+
121+
node.location = nodeLatLng
122+
node.properties.location = nodeLatLng;
123+
data.value = [nodeLatLng.lng, nodeLatLng.lat];
124+
});
125+
// Similary, updating the coordinates for the links
126+
mapOptions.series[1].data.forEach((data) => {
127+
const soruce = data.coords[0];
128+
const sourcePx = Number(soruce[0]);
129+
const sourcePy = -Number(soruce[1]);
130+
const sourceNodeProjected = L.point(topLeft.x + sourcePx, topLeft.y + sourcePy);
131+
const sourceNodeLatLng = map.unproject(sourceNodeProjected, zoom);
132+
133+
const target = data.coords[1];
134+
const targetPx = Number(target[0]);
135+
const targetPy = -Number(target[1]);
136+
const targetNodeProjected = L.point(topLeft.x + targetPx, topLeft.y + targetPy);
137+
const targetNodeLatLng = map.unproject(targetNodeProjected, zoom);
138+
139+
console.log(soruce, sourceNodeLatLng)
140+
console.log(target, targetNodeLatLng)
141+
142+
data.coords[0] = [sourceNodeLatLng.lng, sourceNodeLatLng.lat];
143+
data.coords[1] = [targetNodeLatLng.lng, targetNodeLatLng.lat];
144+
});
145+
this.echarts.setOption(mapOptions);
146+
// Unproject the topLeft and bottomRight points to get northWest and southEast latlngs
147+
const nw = map.unproject(topLeft, zoom);
148+
const se = map.unproject(bottomRight, zoom);
149+
const bnds = L.latLngBounds(nw, se);
150+
151+
L.imageOverlay(imageUrl, bnds).addTo(map);
152+
map.fitBounds(bnds);
153+
map.setMaxBounds(bnds);
154+
},
155+
},
156+
);
152157
graph.render();
153158
</script>
154159
</body>

public/example_templates/netjsonmap.html

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,6 @@
7171
<script type="text/javascript">
7272
const map = new NetJSONGraph("../assets/data/netjsonmap.json", {
7373
render: "map",
74-
bookmarkableActions: {
75-
enabled: true,
76-
id: "geographicMap"
77-
},
7874
mapOptions: {
7975
center: [46.86764405052012, 19.675998687744144],
8076
zoom: 5,
@@ -99,6 +95,11 @@
9995
},
10096
},
10197
],
98+
bookmarkableActions: {
99+
enabled: true,
100+
id: "geographicMap",
101+
zoomLevel: 10,
102+
},
102103

103104
prepareData: (data) => {
104105
data.nodes.map((node) => {

src/js/netjsongraph.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ const NetJSONGraphDefaultConfig = {
288288
bookmarkableActions: {
289289
enabled: false,
290290
id: null,
291+
zoomLevel: null,
291292
},
292293

293294
/**

0 commit comments

Comments
 (0)