@sigma/layer-maplibre 3.0.0-beta.9 → 4.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (C) 2013-2024, Alexis Jacomy, Guillaume Plique, Benoît Simard https://www.sigmajs.org
1
+ Copyright (C) 2013-2025, Alexis Jacomy, Guillaume Plique, Benoît Simard https://www.sigmajs.org
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
4
4
  to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
package/README.md CHANGED
@@ -24,4 +24,4 @@ const sigma = new Sigma(graph, container);
24
24
  bindMaplibreLayer(sigma);
25
25
  ```
26
26
 
27
- Please check the related [Storybook](https://github.com/jacomyal/sigma.js/tree/main/packages/storybook/stories/3-additional-packages/layer-maplibre) for more advanced examples.
27
+ Please check the [website examples](https://www.sigmajs.org/examples/) for more advanced examples.
@@ -11,7 +11,7 @@ import { graphToLatlng, latlngToGraph } from "./utils.js";
11
11
  * @param opts.getNodeLatLng Function to retrieve lat/long values from a node's attributs (default is lat & lng)
12
12
  */
13
13
  export default function bindMaplibreLayer(sigma: Sigma, opts?: {
14
- mapOptions?: Omit<MapOptions, "container" | "center" | "zoom" | "minPitch" | "maxPitch">;
14
+ mapOptions?: Omit<MapOptions, "container" | "center" | "zoom" | "bounds" | "minPitch" | "maxPitch" | "interactive">;
15
15
  getNodeLatLng?: (nodeAttributes: Attributes) => {
16
16
  lat: number;
17
17
  lng: number;
@@ -1,10 +1,7 @@
1
- import { Map } from "maplibre-gl";
2
- import { Sigma } from "sigma";
3
- export declare const MAX_VALID_LATITUDE = 85.051129;
4
1
  /**
5
2
  * Given a geo point returns its graph coords.
6
3
  */
7
- export declare function latlngToGraph(map: Map, coord: {
4
+ export declare function latlngToGraph(coord: {
8
5
  lat: number;
9
6
  lng: number;
10
7
  }): {
@@ -12,20 +9,12 @@ export declare function latlngToGraph(map: Map, coord: {
12
9
  y: number;
13
10
  };
14
11
  /**
15
- * Given a graph coords returns its lat/lng coords.
12
+ * Given graph coords returns its lat/lng coords.
16
13
  */
17
- export declare function graphToLatlng(map: Map, coords: {
14
+ export declare function graphToLatlng(coords: {
18
15
  x: number;
19
16
  y: number;
20
17
  }): {
21
18
  lat: number;
22
19
  lng: number;
23
20
  };
24
- /**
25
- * BBOX sync : map to sigma
26
- */
27
- export declare function syncSigmaWithMap(sigma: Sigma, map: Map): void;
28
- /**
29
- * BBOX sync : sigma to map
30
- */
31
- export declare function syncMapWithSigma(sigma: Sigma, map: Map): void;
@@ -2,6 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var utils = require('@sigma/utils');
5
6
  var maplibreGl = require('maplibre-gl');
6
7
 
7
8
  function _toPrimitive(t, r) {
@@ -54,20 +55,19 @@ function _objectSpread2(e) {
54
55
  /**
55
56
  * Given a geo point returns its graph coords.
56
57
  */
57
- function latlngToGraph(map, coord) {
58
+ function latlngToGraph(coord) {
58
59
  var data = maplibreGl.MercatorCoordinate.fromLngLat(coord);
59
60
  return {
60
61
  x: data.x,
61
- // Y are reversed between geo / sigma
62
- y: map.getContainer().clientHeight - data.y
62
+ y: 1 - data.y
63
63
  };
64
64
  }
65
65
 
66
66
  /**
67
- * Given a graph coords returns its lat/lng coords.
67
+ * Given graph coords returns its lat/lng coords.
68
68
  */
69
- function graphToLatlng(map, coords) {
70
- var mcoords = new maplibreGl.MercatorCoordinate(coords.x, map.getContainer().clientHeight - coords.y, 0);
69
+ function graphToLatlng(coords) {
70
+ var mcoords = new maplibreGl.MercatorCoordinate(coords.x, 1 - coords.y, 0);
71
71
  var data = mcoords.toLngLat();
72
72
  return {
73
73
  lat: data.lat,
@@ -75,60 +75,6 @@ function graphToLatlng(map, coords) {
75
75
  };
76
76
  }
77
77
 
78
- /**
79
- * BBOX sync : map to sigma
80
- */
81
- function syncSigmaWithMap(sigma, map) {
82
- var mapBound = map.getBounds();
83
-
84
- // Compute sigma center
85
- var center = sigma.viewportToFramedGraph(sigma.graphToViewport(latlngToGraph(map, mapBound.getCenter())));
86
-
87
- // Compute sigma ratio
88
- var northEast = sigma.graphToViewport(latlngToGraph(map, mapBound.getNorthEast()));
89
- var southWest = sigma.graphToViewport(latlngToGraph(map, mapBound.getSouthWest()));
90
- var viewportBoundDimension = {
91
- width: Math.abs(northEast.x - southWest.x),
92
- height: Math.abs(northEast.y - southWest.y)
93
- };
94
- var viewportDim = sigma.getDimensions();
95
- var ratio = Math.min(viewportBoundDimension.width / viewportDim.width, viewportBoundDimension.height / viewportDim.height) * sigma.getCamera().getState().ratio;
96
- sigma.getCamera().setState(_objectSpread2(_objectSpread2({}, center), {}, {
97
- ratio: ratio
98
- }));
99
- }
100
-
101
- /**
102
- * BBOX sync : sigma to map
103
- */
104
- function syncMapWithSigma(sigma, map) {
105
- var viewportDimensions = sigma.getDimensions();
106
-
107
- // Graph BBox
108
- var graphBottomLeft = sigma.viewportToGraph({
109
- x: 0,
110
- y: viewportDimensions.height
111
- }, {
112
- padding: 0
113
- });
114
- var graphTopRight = sigma.viewportToGraph({
115
- x: viewportDimensions.width,
116
- y: 0
117
- }, {
118
- padding: 0
119
- });
120
-
121
- // Geo BBox
122
- var geoSouthWest = graphToLatlng(map, graphBottomLeft);
123
- var geoNorthEast = graphToLatlng(map, graphTopRight);
124
-
125
- // Set map BBox
126
- var bounds = new maplibreGl.LngLatBounds([geoSouthWest, geoNorthEast]);
127
- map.fitBounds(bounds, {
128
- duration: 0
129
- });
130
- }
131
-
132
78
  /**
133
79
  * On the graph, we store the 2D projection of the geographical lat/long.
134
80
  *
@@ -140,40 +86,26 @@ function bindMaplibreLayer(sigma, opts) {
140
86
  // Keeping data for the cleanup
141
87
  var isKilled = false;
142
88
  var prevSigmaSettings = sigma.getSettings();
143
-
144
- // Creating map container
145
- var mapLayerName = "layer-maplibre";
146
- var mapContainer = sigma.createLayer(mapLayerName, "div", {
147
- style: {
148
- position: "absolute",
149
- inset: "0"
150
- },
151
- // 'edges' is the first sigma layer
152
- beforeLayer: "edges"
153
- });
154
- sigma.getContainer().prepend(mapContainer);
155
-
156
- // Initialize the map
157
- var map = new maplibreGl.Map(_objectSpread2({
158
- container: mapContainer,
159
- style: "https://demotiles.maplibre.org/style.json",
160
- center: [0, 0],
161
- zoom: 1,
162
- minPitch: 0,
163
- maxPitch: 0
164
- }, (opts === null || opts === void 0 ? void 0 : opts.mapOptions) || {}));
89
+ var prevCustomBBox = sigma.getCustomBBox();
165
90
 
166
91
  // `stagePadding: 0` is mandatory, so the bbox of the map & Sigma is the same.
167
92
  sigma.setSetting("stagePadding", 0);
168
-
169
93
  // disable camera rotation
170
94
  sigma.setSetting("enableCameraRotation", false);
171
95
 
172
- // Function that change the given graph by generating the sigma x,y coords by taking the geo coordinates
173
- // and project them in the 2D space of the map
96
+ // Mercator graph coordinates are in [0,1]. Set customBBox to make
97
+ // normalization identity, and force a synchronous refresh so the
98
+ // normalization function is updated before camera constraints are applied.
99
+ sigma.setCustomBBox({
100
+ x: [0, 1],
101
+ y: [0, 1]
102
+ });
103
+
104
+ // Function that updates graph node positions from geo coordinates to
105
+ // Mercator [0,1] space
174
106
  function updateGraphCoordinates(graph) {
175
107
  graph.updateEachNodeAttributes(function (_node, attrs) {
176
- var coords = latlngToGraph(map, opts !== null && opts !== void 0 && opts.getNodeLatLng ? opts.getNodeLatLng(attrs) : {
108
+ var coords = latlngToGraph(opts !== null && opts !== void 0 && opts.getNodeLatLng ? opts.getNodeLatLng(attrs) : {
177
109
  lat: attrs.lat,
178
110
  lng: attrs.lng
179
111
  });
@@ -184,74 +116,134 @@ function bindMaplibreLayer(sigma, opts) {
184
116
  });
185
117
  }
186
118
 
187
- // Function that sync the map with sigma
188
- function fnSyncMapWithSigma() {
189
- syncMapWithSigma(sigma, map);
119
+ // Update graph coordinates and refresh sigma so normalization is ready
120
+ updateGraphCoordinates(sigma.getGraph());
121
+ sigma.refresh();
122
+
123
+ // Now that normalization is correct, fit the camera to the graph's
124
+ // Mercator extent so the initial view frames the nodes.
125
+ var graph = sigma.getGraph();
126
+ if (graph.order) {
127
+ sigma.getCamera().setState(utils.getCameraStateToFitViewportToNodes(sigma, graph.nodes()));
190
128
  }
191
129
 
192
- // Function that sync sigma with map if it's needed
193
- function fnSyncSigmaWithMap() {
194
- if (!sigma.getCamera().isAnimated() && !map.isMoving()) {
195
- // Check that sigma & map are already in sync
196
- var southWest = graphToLatlng(map, sigma.viewportToGraph({
197
- x: 0,
198
- y: sigma.getDimensions().height
199
- }));
200
- var northEast = graphToLatlng(map, sigma.viewportToGraph({
201
- x: sigma.getDimensions().width,
202
- y: 0
203
- }));
204
- var diff = Math.max(map.getBounds().getSouthWest().distanceTo(new maplibreGl.LngLat(southWest.lng, southWest.lat)), map.getBounds().getNorthEast().distanceTo(new maplibreGl.LngLat(northEast.lng, northEast.lat)));
205
- if (diff > 1) {
206
- syncSigmaWithMap(sigma, map);
207
- }
130
+ // Apply camera constraints. cameraPanBoundaries triggers cleanCameraState
131
+ // which uses graphToViewport — it needs the correct normalization.
132
+ sigma.setSetting("cameraPanBoundaries", {
133
+ boundaries: {
134
+ x: [-1, 2],
135
+ y: [0, 1]
208
136
  }
137
+ });
138
+
139
+ // Prevent zooming out beyond the world. The max ratio depends on the
140
+ // viewport aspect ratio: for a square graph, the Y range [0,1] fills the
141
+ // viewport height when ratio = min(width, height) / height.
142
+ function updateMaxCameraRatio() {
143
+ var _sigma$getDimensions = sigma.getDimensions(),
144
+ width = _sigma$getDimensions.width,
145
+ height = _sigma$getDimensions.height;
146
+ sigma.setSetting("maxCameraRatio", Math.min(width, height) / height);
147
+ }
148
+ updateMaxCameraRatio();
149
+
150
+ // Compute initial map bounds from sigma's current viewport, so the map's
151
+ // first render is already at the correct position
152
+ function getSigmaViewportBounds() {
153
+ var dims = sigma.getDimensions();
154
+ var graphBottomLeft = sigma.viewportToGraph({
155
+ x: 0,
156
+ y: dims.height
157
+ }, {
158
+ padding: 0
159
+ });
160
+ var graphTopRight = sigma.viewportToGraph({
161
+ x: dims.width,
162
+ y: 0
163
+ }, {
164
+ padding: 0
165
+ });
166
+ return new maplibreGl.LngLatBounds(graphToLatlng(graphBottomLeft), graphToLatlng(graphTopRight));
167
+ }
168
+
169
+ // Creating map container
170
+ var mapLayerName = "layer-maplibre";
171
+ var mapContainer = sigma.createLayer(mapLayerName, "div", {
172
+ style: {
173
+ position: "absolute",
174
+ inset: "0"
175
+ },
176
+ // 'stage' is the first sigma layer
177
+ beforeLayer: "stage"
178
+ });
179
+ sigma.getContainer().prepend(mapContainer);
180
+
181
+ // Initialize the map (non-interactive: sigma controls all interaction)
182
+ var map = new maplibreGl.Map(_objectSpread2(_objectSpread2({
183
+ container: mapContainer,
184
+ style: "https://demotiles.maplibre.org/style.json",
185
+ minPitch: 0,
186
+ maxPitch: 0
187
+ }, opts === null || opts === void 0 ? void 0 : opts.mapOptions), {}, {
188
+ interactive: false,
189
+ bounds: getSigmaViewportBounds()
190
+ }));
191
+
192
+ // Camera state memoization to skip redundant fitBounds calls
193
+ var lx = NaN,
194
+ ly = NaN,
195
+ lr = NaN,
196
+ la = NaN;
197
+
198
+ // Sync map bounds to match sigma's viewport
199
+ function syncMapFromSigma() {
200
+ var _sigma$getCamera$getS = sigma.getCamera().getState(),
201
+ x = _sigma$getCamera$getS.x,
202
+ y = _sigma$getCamera$getS.y,
203
+ ratio = _sigma$getCamera$getS.ratio,
204
+ angle = _sigma$getCamera$getS.angle;
205
+ if (x === lx && y === ly && ratio === lr && angle === la) return;
206
+ lx = x;
207
+ ly = y;
208
+ lr = ratio;
209
+ la = angle;
210
+ map.fitBounds(getSigmaViewportBounds(), {
211
+ duration: 0
212
+ });
209
213
  }
210
214
 
211
- // When sigma is resize, we need to update the graph coordinate (the ref has changed)
212
- // and recompute the zoom bounds
215
+ // When sigma is resized, resize the map, recompute zoom limit, and re-sync
213
216
  function fnOnResize() {
214
- updateGraphCoordinates(sigma.getGraph());
215
- fnSyncSigmaWithMap();
217
+ map.resize();
218
+ updateMaxCameraRatio();
219
+ lx = NaN;
220
+ syncMapFromSigma();
216
221
  }
217
222
 
218
223
  // Clean up function to remove everything
219
224
  function clean() {
220
225
  if (!isKilled) {
221
226
  isKilled = true;
222
- map.off("moveend", fnSyncSigmaWithMap);
223
227
  map.remove();
224
228
  sigma.killLayer(mapLayerName);
225
- sigma.off("afterRender", fnSyncMapWithSigma);
229
+ sigma.off("afterRender", syncMapFromSigma);
226
230
  sigma.off("resize", fnOnResize);
231
+ sigma.off("kill", clean);
227
232
 
228
233
  // Reset settings
229
234
  sigma.setSetting("stagePadding", prevSigmaSettings.stagePadding);
230
235
  sigma.setSetting("enableCameraRotation", prevSigmaSettings.enableCameraRotation);
236
+ sigma.setSetting("maxCameraRatio", prevSigmaSettings.maxCameraRatio);
237
+ sigma.setSetting("cameraPanBoundaries", prevSigmaSettings.cameraPanBoundaries);
238
+ sigma.setCustomBBox(prevCustomBBox);
231
239
  }
232
240
  }
233
241
 
234
- // Update the sigma graph for geospatial coords
235
- updateGraphCoordinates(sigma.getGraph());
236
-
237
- // When the map is ready
242
+ // When the map is ready, start syncing
238
243
  map.once("load", function () {
239
- // Compute sigma ratio bounds
240
- // /!\ must be done after the first map render
241
- map.once("moveend", function () {
242
- fnSyncSigmaWithMap();
243
- });
244
- // Do the first sync
245
- fnSyncMapWithSigma();
246
-
247
- // At each render of sigma, we do the map sync
248
- sigma.on("afterRender", fnSyncMapWithSigma);
249
- // Listen on resize
244
+ sigma.on("afterRender", syncMapFromSigma);
250
245
  sigma.on("resize", fnOnResize);
251
- // Do the cleanup
252
246
  sigma.on("kill", clean);
253
- // Keep sigma camera on the map
254
- map.on("moveend", fnSyncSigmaWithMap);
255
247
  });
256
248
  return {
257
249
  clean: clean,
@@ -2,6 +2,7 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
+ var utils = require('@sigma/utils');
5
6
  var maplibreGl = require('maplibre-gl');
6
7
 
7
8
  function _toPrimitive(t, r) {
@@ -54,20 +55,19 @@ function _objectSpread2(e) {
54
55
  /**
55
56
  * Given a geo point returns its graph coords.
56
57
  */
57
- function latlngToGraph(map, coord) {
58
+ function latlngToGraph(coord) {
58
59
  var data = maplibreGl.MercatorCoordinate.fromLngLat(coord);
59
60
  return {
60
61
  x: data.x,
61
- // Y are reversed between geo / sigma
62
- y: map.getContainer().clientHeight - data.y
62
+ y: 1 - data.y
63
63
  };
64
64
  }
65
65
 
66
66
  /**
67
- * Given a graph coords returns its lat/lng coords.
67
+ * Given graph coords returns its lat/lng coords.
68
68
  */
69
- function graphToLatlng(map, coords) {
70
- var mcoords = new maplibreGl.MercatorCoordinate(coords.x, map.getContainer().clientHeight - coords.y, 0);
69
+ function graphToLatlng(coords) {
70
+ var mcoords = new maplibreGl.MercatorCoordinate(coords.x, 1 - coords.y, 0);
71
71
  var data = mcoords.toLngLat();
72
72
  return {
73
73
  lat: data.lat,
@@ -75,60 +75,6 @@ function graphToLatlng(map, coords) {
75
75
  };
76
76
  }
77
77
 
78
- /**
79
- * BBOX sync : map to sigma
80
- */
81
- function syncSigmaWithMap(sigma, map) {
82
- var mapBound = map.getBounds();
83
-
84
- // Compute sigma center
85
- var center = sigma.viewportToFramedGraph(sigma.graphToViewport(latlngToGraph(map, mapBound.getCenter())));
86
-
87
- // Compute sigma ratio
88
- var northEast = sigma.graphToViewport(latlngToGraph(map, mapBound.getNorthEast()));
89
- var southWest = sigma.graphToViewport(latlngToGraph(map, mapBound.getSouthWest()));
90
- var viewportBoundDimension = {
91
- width: Math.abs(northEast.x - southWest.x),
92
- height: Math.abs(northEast.y - southWest.y)
93
- };
94
- var viewportDim = sigma.getDimensions();
95
- var ratio = Math.min(viewportBoundDimension.width / viewportDim.width, viewportBoundDimension.height / viewportDim.height) * sigma.getCamera().getState().ratio;
96
- sigma.getCamera().setState(_objectSpread2(_objectSpread2({}, center), {}, {
97
- ratio: ratio
98
- }));
99
- }
100
-
101
- /**
102
- * BBOX sync : sigma to map
103
- */
104
- function syncMapWithSigma(sigma, map) {
105
- var viewportDimensions = sigma.getDimensions();
106
-
107
- // Graph BBox
108
- var graphBottomLeft = sigma.viewportToGraph({
109
- x: 0,
110
- y: viewportDimensions.height
111
- }, {
112
- padding: 0
113
- });
114
- var graphTopRight = sigma.viewportToGraph({
115
- x: viewportDimensions.width,
116
- y: 0
117
- }, {
118
- padding: 0
119
- });
120
-
121
- // Geo BBox
122
- var geoSouthWest = graphToLatlng(map, graphBottomLeft);
123
- var geoNorthEast = graphToLatlng(map, graphTopRight);
124
-
125
- // Set map BBox
126
- var bounds = new maplibreGl.LngLatBounds([geoSouthWest, geoNorthEast]);
127
- map.fitBounds(bounds, {
128
- duration: 0
129
- });
130
- }
131
-
132
78
  /**
133
79
  * On the graph, we store the 2D projection of the geographical lat/long.
134
80
  *
@@ -140,40 +86,26 @@ function bindMaplibreLayer(sigma, opts) {
140
86
  // Keeping data for the cleanup
141
87
  var isKilled = false;
142
88
  var prevSigmaSettings = sigma.getSettings();
143
-
144
- // Creating map container
145
- var mapLayerName = "layer-maplibre";
146
- var mapContainer = sigma.createLayer(mapLayerName, "div", {
147
- style: {
148
- position: "absolute",
149
- inset: "0"
150
- },
151
- // 'edges' is the first sigma layer
152
- beforeLayer: "edges"
153
- });
154
- sigma.getContainer().prepend(mapContainer);
155
-
156
- // Initialize the map
157
- var map = new maplibreGl.Map(_objectSpread2({
158
- container: mapContainer,
159
- style: "https://demotiles.maplibre.org/style.json",
160
- center: [0, 0],
161
- zoom: 1,
162
- minPitch: 0,
163
- maxPitch: 0
164
- }, (opts === null || opts === void 0 ? void 0 : opts.mapOptions) || {}));
89
+ var prevCustomBBox = sigma.getCustomBBox();
165
90
 
166
91
  // `stagePadding: 0` is mandatory, so the bbox of the map & Sigma is the same.
167
92
  sigma.setSetting("stagePadding", 0);
168
-
169
93
  // disable camera rotation
170
94
  sigma.setSetting("enableCameraRotation", false);
171
95
 
172
- // Function that change the given graph by generating the sigma x,y coords by taking the geo coordinates
173
- // and project them in the 2D space of the map
96
+ // Mercator graph coordinates are in [0,1]. Set customBBox to make
97
+ // normalization identity, and force a synchronous refresh so the
98
+ // normalization function is updated before camera constraints are applied.
99
+ sigma.setCustomBBox({
100
+ x: [0, 1],
101
+ y: [0, 1]
102
+ });
103
+
104
+ // Function that updates graph node positions from geo coordinates to
105
+ // Mercator [0,1] space
174
106
  function updateGraphCoordinates(graph) {
175
107
  graph.updateEachNodeAttributes(function (_node, attrs) {
176
- var coords = latlngToGraph(map, opts !== null && opts !== void 0 && opts.getNodeLatLng ? opts.getNodeLatLng(attrs) : {
108
+ var coords = latlngToGraph(opts !== null && opts !== void 0 && opts.getNodeLatLng ? opts.getNodeLatLng(attrs) : {
177
109
  lat: attrs.lat,
178
110
  lng: attrs.lng
179
111
  });
@@ -184,74 +116,134 @@ function bindMaplibreLayer(sigma, opts) {
184
116
  });
185
117
  }
186
118
 
187
- // Function that sync the map with sigma
188
- function fnSyncMapWithSigma() {
189
- syncMapWithSigma(sigma, map);
119
+ // Update graph coordinates and refresh sigma so normalization is ready
120
+ updateGraphCoordinates(sigma.getGraph());
121
+ sigma.refresh();
122
+
123
+ // Now that normalization is correct, fit the camera to the graph's
124
+ // Mercator extent so the initial view frames the nodes.
125
+ var graph = sigma.getGraph();
126
+ if (graph.order) {
127
+ sigma.getCamera().setState(utils.getCameraStateToFitViewportToNodes(sigma, graph.nodes()));
190
128
  }
191
129
 
192
- // Function that sync sigma with map if it's needed
193
- function fnSyncSigmaWithMap() {
194
- if (!sigma.getCamera().isAnimated() && !map.isMoving()) {
195
- // Check that sigma & map are already in sync
196
- var southWest = graphToLatlng(map, sigma.viewportToGraph({
197
- x: 0,
198
- y: sigma.getDimensions().height
199
- }));
200
- var northEast = graphToLatlng(map, sigma.viewportToGraph({
201
- x: sigma.getDimensions().width,
202
- y: 0
203
- }));
204
- var diff = Math.max(map.getBounds().getSouthWest().distanceTo(new maplibreGl.LngLat(southWest.lng, southWest.lat)), map.getBounds().getNorthEast().distanceTo(new maplibreGl.LngLat(northEast.lng, northEast.lat)));
205
- if (diff > 1) {
206
- syncSigmaWithMap(sigma, map);
207
- }
130
+ // Apply camera constraints. cameraPanBoundaries triggers cleanCameraState
131
+ // which uses graphToViewport — it needs the correct normalization.
132
+ sigma.setSetting("cameraPanBoundaries", {
133
+ boundaries: {
134
+ x: [-1, 2],
135
+ y: [0, 1]
208
136
  }
137
+ });
138
+
139
+ // Prevent zooming out beyond the world. The max ratio depends on the
140
+ // viewport aspect ratio: for a square graph, the Y range [0,1] fills the
141
+ // viewport height when ratio = min(width, height) / height.
142
+ function updateMaxCameraRatio() {
143
+ var _sigma$getDimensions = sigma.getDimensions(),
144
+ width = _sigma$getDimensions.width,
145
+ height = _sigma$getDimensions.height;
146
+ sigma.setSetting("maxCameraRatio", Math.min(width, height) / height);
147
+ }
148
+ updateMaxCameraRatio();
149
+
150
+ // Compute initial map bounds from sigma's current viewport, so the map's
151
+ // first render is already at the correct position
152
+ function getSigmaViewportBounds() {
153
+ var dims = sigma.getDimensions();
154
+ var graphBottomLeft = sigma.viewportToGraph({
155
+ x: 0,
156
+ y: dims.height
157
+ }, {
158
+ padding: 0
159
+ });
160
+ var graphTopRight = sigma.viewportToGraph({
161
+ x: dims.width,
162
+ y: 0
163
+ }, {
164
+ padding: 0
165
+ });
166
+ return new maplibreGl.LngLatBounds(graphToLatlng(graphBottomLeft), graphToLatlng(graphTopRight));
167
+ }
168
+
169
+ // Creating map container
170
+ var mapLayerName = "layer-maplibre";
171
+ var mapContainer = sigma.createLayer(mapLayerName, "div", {
172
+ style: {
173
+ position: "absolute",
174
+ inset: "0"
175
+ },
176
+ // 'stage' is the first sigma layer
177
+ beforeLayer: "stage"
178
+ });
179
+ sigma.getContainer().prepend(mapContainer);
180
+
181
+ // Initialize the map (non-interactive: sigma controls all interaction)
182
+ var map = new maplibreGl.Map(_objectSpread2(_objectSpread2({
183
+ container: mapContainer,
184
+ style: "https://demotiles.maplibre.org/style.json",
185
+ minPitch: 0,
186
+ maxPitch: 0
187
+ }, opts === null || opts === void 0 ? void 0 : opts.mapOptions), {}, {
188
+ interactive: false,
189
+ bounds: getSigmaViewportBounds()
190
+ }));
191
+
192
+ // Camera state memoization to skip redundant fitBounds calls
193
+ var lx = NaN,
194
+ ly = NaN,
195
+ lr = NaN,
196
+ la = NaN;
197
+
198
+ // Sync map bounds to match sigma's viewport
199
+ function syncMapFromSigma() {
200
+ var _sigma$getCamera$getS = sigma.getCamera().getState(),
201
+ x = _sigma$getCamera$getS.x,
202
+ y = _sigma$getCamera$getS.y,
203
+ ratio = _sigma$getCamera$getS.ratio,
204
+ angle = _sigma$getCamera$getS.angle;
205
+ if (x === lx && y === ly && ratio === lr && angle === la) return;
206
+ lx = x;
207
+ ly = y;
208
+ lr = ratio;
209
+ la = angle;
210
+ map.fitBounds(getSigmaViewportBounds(), {
211
+ duration: 0
212
+ });
209
213
  }
210
214
 
211
- // When sigma is resize, we need to update the graph coordinate (the ref has changed)
212
- // and recompute the zoom bounds
215
+ // When sigma is resized, resize the map, recompute zoom limit, and re-sync
213
216
  function fnOnResize() {
214
- updateGraphCoordinates(sigma.getGraph());
215
- fnSyncSigmaWithMap();
217
+ map.resize();
218
+ updateMaxCameraRatio();
219
+ lx = NaN;
220
+ syncMapFromSigma();
216
221
  }
217
222
 
218
223
  // Clean up function to remove everything
219
224
  function clean() {
220
225
  if (!isKilled) {
221
226
  isKilled = true;
222
- map.off("moveend", fnSyncSigmaWithMap);
223
227
  map.remove();
224
228
  sigma.killLayer(mapLayerName);
225
- sigma.off("afterRender", fnSyncMapWithSigma);
229
+ sigma.off("afterRender", syncMapFromSigma);
226
230
  sigma.off("resize", fnOnResize);
231
+ sigma.off("kill", clean);
227
232
 
228
233
  // Reset settings
229
234
  sigma.setSetting("stagePadding", prevSigmaSettings.stagePadding);
230
235
  sigma.setSetting("enableCameraRotation", prevSigmaSettings.enableCameraRotation);
236
+ sigma.setSetting("maxCameraRatio", prevSigmaSettings.maxCameraRatio);
237
+ sigma.setSetting("cameraPanBoundaries", prevSigmaSettings.cameraPanBoundaries);
238
+ sigma.setCustomBBox(prevCustomBBox);
231
239
  }
232
240
  }
233
241
 
234
- // Update the sigma graph for geospatial coords
235
- updateGraphCoordinates(sigma.getGraph());
236
-
237
- // When the map is ready
242
+ // When the map is ready, start syncing
238
243
  map.once("load", function () {
239
- // Compute sigma ratio bounds
240
- // /!\ must be done after the first map render
241
- map.once("moveend", function () {
242
- fnSyncSigmaWithMap();
243
- });
244
- // Do the first sync
245
- fnSyncMapWithSigma();
246
-
247
- // At each render of sigma, we do the map sync
248
- sigma.on("afterRender", fnSyncMapWithSigma);
249
- // Listen on resize
244
+ sigma.on("afterRender", syncMapFromSigma);
250
245
  sigma.on("resize", fnOnResize);
251
- // Do the cleanup
252
246
  sigma.on("kill", clean);
253
- // Keep sigma camera on the map
254
- map.on("moveend", fnSyncSigmaWithMap);
255
247
  });
256
248
  return {
257
249
  clean: clean,
@@ -1,4 +1,5 @@
1
- import { MercatorCoordinate, LngLatBounds, Map, LngLat } from 'maplibre-gl';
1
+ import { getCameraStateToFitViewportToNodes } from '@sigma/utils';
2
+ import { MercatorCoordinate, Map, LngLatBounds } from 'maplibre-gl';
2
3
 
3
4
  function _toPrimitive(t, r) {
4
5
  if ("object" != typeof t || !t) return t;
@@ -50,20 +51,19 @@ function _objectSpread2(e) {
50
51
  /**
51
52
  * Given a geo point returns its graph coords.
52
53
  */
53
- function latlngToGraph(map, coord) {
54
+ function latlngToGraph(coord) {
54
55
  var data = MercatorCoordinate.fromLngLat(coord);
55
56
  return {
56
57
  x: data.x,
57
- // Y are reversed between geo / sigma
58
- y: map.getContainer().clientHeight - data.y
58
+ y: 1 - data.y
59
59
  };
60
60
  }
61
61
 
62
62
  /**
63
- * Given a graph coords returns its lat/lng coords.
63
+ * Given graph coords returns its lat/lng coords.
64
64
  */
65
- function graphToLatlng(map, coords) {
66
- var mcoords = new MercatorCoordinate(coords.x, map.getContainer().clientHeight - coords.y, 0);
65
+ function graphToLatlng(coords) {
66
+ var mcoords = new MercatorCoordinate(coords.x, 1 - coords.y, 0);
67
67
  var data = mcoords.toLngLat();
68
68
  return {
69
69
  lat: data.lat,
@@ -71,60 +71,6 @@ function graphToLatlng(map, coords) {
71
71
  };
72
72
  }
73
73
 
74
- /**
75
- * BBOX sync : map to sigma
76
- */
77
- function syncSigmaWithMap(sigma, map) {
78
- var mapBound = map.getBounds();
79
-
80
- // Compute sigma center
81
- var center = sigma.viewportToFramedGraph(sigma.graphToViewport(latlngToGraph(map, mapBound.getCenter())));
82
-
83
- // Compute sigma ratio
84
- var northEast = sigma.graphToViewport(latlngToGraph(map, mapBound.getNorthEast()));
85
- var southWest = sigma.graphToViewport(latlngToGraph(map, mapBound.getSouthWest()));
86
- var viewportBoundDimension = {
87
- width: Math.abs(northEast.x - southWest.x),
88
- height: Math.abs(northEast.y - southWest.y)
89
- };
90
- var viewportDim = sigma.getDimensions();
91
- var ratio = Math.min(viewportBoundDimension.width / viewportDim.width, viewportBoundDimension.height / viewportDim.height) * sigma.getCamera().getState().ratio;
92
- sigma.getCamera().setState(_objectSpread2(_objectSpread2({}, center), {}, {
93
- ratio: ratio
94
- }));
95
- }
96
-
97
- /**
98
- * BBOX sync : sigma to map
99
- */
100
- function syncMapWithSigma(sigma, map) {
101
- var viewportDimensions = sigma.getDimensions();
102
-
103
- // Graph BBox
104
- var graphBottomLeft = sigma.viewportToGraph({
105
- x: 0,
106
- y: viewportDimensions.height
107
- }, {
108
- padding: 0
109
- });
110
- var graphTopRight = sigma.viewportToGraph({
111
- x: viewportDimensions.width,
112
- y: 0
113
- }, {
114
- padding: 0
115
- });
116
-
117
- // Geo BBox
118
- var geoSouthWest = graphToLatlng(map, graphBottomLeft);
119
- var geoNorthEast = graphToLatlng(map, graphTopRight);
120
-
121
- // Set map BBox
122
- var bounds = new LngLatBounds([geoSouthWest, geoNorthEast]);
123
- map.fitBounds(bounds, {
124
- duration: 0
125
- });
126
- }
127
-
128
74
  /**
129
75
  * On the graph, we store the 2D projection of the geographical lat/long.
130
76
  *
@@ -136,40 +82,26 @@ function bindMaplibreLayer(sigma, opts) {
136
82
  // Keeping data for the cleanup
137
83
  var isKilled = false;
138
84
  var prevSigmaSettings = sigma.getSettings();
139
-
140
- // Creating map container
141
- var mapLayerName = "layer-maplibre";
142
- var mapContainer = sigma.createLayer(mapLayerName, "div", {
143
- style: {
144
- position: "absolute",
145
- inset: "0"
146
- },
147
- // 'edges' is the first sigma layer
148
- beforeLayer: "edges"
149
- });
150
- sigma.getContainer().prepend(mapContainer);
151
-
152
- // Initialize the map
153
- var map = new Map(_objectSpread2({
154
- container: mapContainer,
155
- style: "https://demotiles.maplibre.org/style.json",
156
- center: [0, 0],
157
- zoom: 1,
158
- minPitch: 0,
159
- maxPitch: 0
160
- }, (opts === null || opts === void 0 ? void 0 : opts.mapOptions) || {}));
85
+ var prevCustomBBox = sigma.getCustomBBox();
161
86
 
162
87
  // `stagePadding: 0` is mandatory, so the bbox of the map & Sigma is the same.
163
88
  sigma.setSetting("stagePadding", 0);
164
-
165
89
  // disable camera rotation
166
90
  sigma.setSetting("enableCameraRotation", false);
167
91
 
168
- // Function that change the given graph by generating the sigma x,y coords by taking the geo coordinates
169
- // and project them in the 2D space of the map
92
+ // Mercator graph coordinates are in [0,1]. Set customBBox to make
93
+ // normalization identity, and force a synchronous refresh so the
94
+ // normalization function is updated before camera constraints are applied.
95
+ sigma.setCustomBBox({
96
+ x: [0, 1],
97
+ y: [0, 1]
98
+ });
99
+
100
+ // Function that updates graph node positions from geo coordinates to
101
+ // Mercator [0,1] space
170
102
  function updateGraphCoordinates(graph) {
171
103
  graph.updateEachNodeAttributes(function (_node, attrs) {
172
- var coords = latlngToGraph(map, opts !== null && opts !== void 0 && opts.getNodeLatLng ? opts.getNodeLatLng(attrs) : {
104
+ var coords = latlngToGraph(opts !== null && opts !== void 0 && opts.getNodeLatLng ? opts.getNodeLatLng(attrs) : {
173
105
  lat: attrs.lat,
174
106
  lng: attrs.lng
175
107
  });
@@ -180,74 +112,134 @@ function bindMaplibreLayer(sigma, opts) {
180
112
  });
181
113
  }
182
114
 
183
- // Function that sync the map with sigma
184
- function fnSyncMapWithSigma() {
185
- syncMapWithSigma(sigma, map);
115
+ // Update graph coordinates and refresh sigma so normalization is ready
116
+ updateGraphCoordinates(sigma.getGraph());
117
+ sigma.refresh();
118
+
119
+ // Now that normalization is correct, fit the camera to the graph's
120
+ // Mercator extent so the initial view frames the nodes.
121
+ var graph = sigma.getGraph();
122
+ if (graph.order) {
123
+ sigma.getCamera().setState(getCameraStateToFitViewportToNodes(sigma, graph.nodes()));
186
124
  }
187
125
 
188
- // Function that sync sigma with map if it's needed
189
- function fnSyncSigmaWithMap() {
190
- if (!sigma.getCamera().isAnimated() && !map.isMoving()) {
191
- // Check that sigma & map are already in sync
192
- var southWest = graphToLatlng(map, sigma.viewportToGraph({
193
- x: 0,
194
- y: sigma.getDimensions().height
195
- }));
196
- var northEast = graphToLatlng(map, sigma.viewportToGraph({
197
- x: sigma.getDimensions().width,
198
- y: 0
199
- }));
200
- var diff = Math.max(map.getBounds().getSouthWest().distanceTo(new LngLat(southWest.lng, southWest.lat)), map.getBounds().getNorthEast().distanceTo(new LngLat(northEast.lng, northEast.lat)));
201
- if (diff > 1) {
202
- syncSigmaWithMap(sigma, map);
203
- }
126
+ // Apply camera constraints. cameraPanBoundaries triggers cleanCameraState
127
+ // which uses graphToViewport — it needs the correct normalization.
128
+ sigma.setSetting("cameraPanBoundaries", {
129
+ boundaries: {
130
+ x: [-1, 2],
131
+ y: [0, 1]
204
132
  }
133
+ });
134
+
135
+ // Prevent zooming out beyond the world. The max ratio depends on the
136
+ // viewport aspect ratio: for a square graph, the Y range [0,1] fills the
137
+ // viewport height when ratio = min(width, height) / height.
138
+ function updateMaxCameraRatio() {
139
+ var _sigma$getDimensions = sigma.getDimensions(),
140
+ width = _sigma$getDimensions.width,
141
+ height = _sigma$getDimensions.height;
142
+ sigma.setSetting("maxCameraRatio", Math.min(width, height) / height);
143
+ }
144
+ updateMaxCameraRatio();
145
+
146
+ // Compute initial map bounds from sigma's current viewport, so the map's
147
+ // first render is already at the correct position
148
+ function getSigmaViewportBounds() {
149
+ var dims = sigma.getDimensions();
150
+ var graphBottomLeft = sigma.viewportToGraph({
151
+ x: 0,
152
+ y: dims.height
153
+ }, {
154
+ padding: 0
155
+ });
156
+ var graphTopRight = sigma.viewportToGraph({
157
+ x: dims.width,
158
+ y: 0
159
+ }, {
160
+ padding: 0
161
+ });
162
+ return new LngLatBounds(graphToLatlng(graphBottomLeft), graphToLatlng(graphTopRight));
205
163
  }
206
164
 
207
- // When sigma is resize, we need to update the graph coordinate (the ref has changed)
208
- // and recompute the zoom bounds
165
+ // Creating map container
166
+ var mapLayerName = "layer-maplibre";
167
+ var mapContainer = sigma.createLayer(mapLayerName, "div", {
168
+ style: {
169
+ position: "absolute",
170
+ inset: "0"
171
+ },
172
+ // 'stage' is the first sigma layer
173
+ beforeLayer: "stage"
174
+ });
175
+ sigma.getContainer().prepend(mapContainer);
176
+
177
+ // Initialize the map (non-interactive: sigma controls all interaction)
178
+ var map = new Map(_objectSpread2(_objectSpread2({
179
+ container: mapContainer,
180
+ style: "https://demotiles.maplibre.org/style.json",
181
+ minPitch: 0,
182
+ maxPitch: 0
183
+ }, opts === null || opts === void 0 ? void 0 : opts.mapOptions), {}, {
184
+ interactive: false,
185
+ bounds: getSigmaViewportBounds()
186
+ }));
187
+
188
+ // Camera state memoization to skip redundant fitBounds calls
189
+ var lx = NaN,
190
+ ly = NaN,
191
+ lr = NaN,
192
+ la = NaN;
193
+
194
+ // Sync map bounds to match sigma's viewport
195
+ function syncMapFromSigma() {
196
+ var _sigma$getCamera$getS = sigma.getCamera().getState(),
197
+ x = _sigma$getCamera$getS.x,
198
+ y = _sigma$getCamera$getS.y,
199
+ ratio = _sigma$getCamera$getS.ratio,
200
+ angle = _sigma$getCamera$getS.angle;
201
+ if (x === lx && y === ly && ratio === lr && angle === la) return;
202
+ lx = x;
203
+ ly = y;
204
+ lr = ratio;
205
+ la = angle;
206
+ map.fitBounds(getSigmaViewportBounds(), {
207
+ duration: 0
208
+ });
209
+ }
210
+
211
+ // When sigma is resized, resize the map, recompute zoom limit, and re-sync
209
212
  function fnOnResize() {
210
- updateGraphCoordinates(sigma.getGraph());
211
- fnSyncSigmaWithMap();
213
+ map.resize();
214
+ updateMaxCameraRatio();
215
+ lx = NaN;
216
+ syncMapFromSigma();
212
217
  }
213
218
 
214
219
  // Clean up function to remove everything
215
220
  function clean() {
216
221
  if (!isKilled) {
217
222
  isKilled = true;
218
- map.off("moveend", fnSyncSigmaWithMap);
219
223
  map.remove();
220
224
  sigma.killLayer(mapLayerName);
221
- sigma.off("afterRender", fnSyncMapWithSigma);
225
+ sigma.off("afterRender", syncMapFromSigma);
222
226
  sigma.off("resize", fnOnResize);
227
+ sigma.off("kill", clean);
223
228
 
224
229
  // Reset settings
225
230
  sigma.setSetting("stagePadding", prevSigmaSettings.stagePadding);
226
231
  sigma.setSetting("enableCameraRotation", prevSigmaSettings.enableCameraRotation);
232
+ sigma.setSetting("maxCameraRatio", prevSigmaSettings.maxCameraRatio);
233
+ sigma.setSetting("cameraPanBoundaries", prevSigmaSettings.cameraPanBoundaries);
234
+ sigma.setCustomBBox(prevCustomBBox);
227
235
  }
228
236
  }
229
237
 
230
- // Update the sigma graph for geospatial coords
231
- updateGraphCoordinates(sigma.getGraph());
232
-
233
- // When the map is ready
238
+ // When the map is ready, start syncing
234
239
  map.once("load", function () {
235
- // Compute sigma ratio bounds
236
- // /!\ must be done after the first map render
237
- map.once("moveend", function () {
238
- fnSyncSigmaWithMap();
239
- });
240
- // Do the first sync
241
- fnSyncMapWithSigma();
242
-
243
- // At each render of sigma, we do the map sync
244
- sigma.on("afterRender", fnSyncMapWithSigma);
245
- // Listen on resize
240
+ sigma.on("afterRender", syncMapFromSigma);
246
241
  sigma.on("resize", fnOnResize);
247
- // Do the cleanup
248
242
  sigma.on("kill", clean);
249
- // Keep sigma camera on the map
250
- map.on("moveend", fnSyncSigmaWithMap);
251
243
  });
252
244
  return {
253
245
  clean: clean,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigma/layer-maplibre",
3
- "version": "3.0.0-beta.9",
3
+ "version": "4.0.0-alpha.0",
4
4
  "description": "A plugin to set a geographical map in background",
5
5
  "main": "dist/sigma-layer-maplibre.cjs.js",
6
6
  "module": "dist/sigma-layer-maplibre.esm.js",
@@ -28,14 +28,20 @@
28
28
  }
29
29
  ],
30
30
  "license": "MIT",
31
+ "scripts": {
32
+ "test": "vitest run"
33
+ },
31
34
  "preconstruct": {
32
35
  "entrypoints": [
33
36
  "index.ts"
34
37
  ]
35
38
  },
39
+ "dependencies": {
40
+ "@sigma/utils": "^4.0.0-alpha.0"
41
+ },
36
42
  "peerDependencies": {
37
43
  "maplibre-gl": "^4.5.0",
38
- "sigma": ">=3.0.0-beta.10"
44
+ "sigma": ">=4.0.0-alpha.0"
39
45
  },
40
46
  "exports": {
41
47
  ".": {
@@ -45,5 +51,5 @@
45
51
  },
46
52
  "./package.json": "./package.json"
47
53
  },
48
- "gitHead": "fc828dcb6e14282f323f2f1c62987444f9cf0c6f"
54
+ "gitHead": "b5c8ef7ea133f5d16282db7ad99ba7c396e7b37c"
49
55
  }