-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[@sigma/layer-leaflet] Adding leaflet map in graph background
* Update the sigma's graph with the projected lat/lng coordinates * Sync the sigma & leaflet view box * Add limits on sigma zoom ratio to not be outside the leaflet capabilities
- Loading branch information
Showing
16 changed files
with
323,015 additions
and
3 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
node_modules | ||
dist |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
.gitignore | ||
node_modules | ||
src | ||
tsconfig.json |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
# Sigma.js Leaflet background layer | ||
|
||
This package contains a leaflet backgournd layer for [sigma.js](https://sigmajs.org). | ||
|
||
It displays a map on the graph's background and handle the camera synchronisation. | ||
|
||
## How to use | ||
|
||
First you need to install [leaflet](https://leafletjs.com/) in your application. | ||
You can check this [page](https://leafletjs.com/download.html) to see how to do it. | ||
|
||
Then, within your application that uses sigma.js, you can use [`@sigma/layer-leaflet`](https://www.npmjs.com/package/@sigma/layer-leaflet) as following: | ||
|
||
```typescript | ||
import bindLeafletLayer from "@sigma/layer-leaflet"; | ||
|
||
const graph = new Graph(); | ||
graph.addNode("nantes", { x: 0, y: 0, lat:47.2308, lng:1.5566, size: 10, label: "nantes" }); | ||
graph.addNode("paris", { x: 0, y: 0, lat: 48.8567, lng:2.3510, size: 10, label: "Paris" }); | ||
graph.addEdge("nantes", "paris"); | ||
|
||
const sigma = new Sigma(graph, container); | ||
bindLeafletLayer(sigma); | ||
``` | ||
|
||
Please check the related [Storybook](https://github.com/jacomyal/sigma.js/tree/main/packages/storybook/stories/layer-leaflet) for more advanced examples. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
{ | ||
"name": "@sigma/layer-leaflet", | ||
"version": "3.0.0-beta.13", | ||
"description": "A plugin to set a geographical map in background", | ||
"main": "dist/sigma-layer-leaflet.cjs.js", | ||
"module": "dist/sigma-layer-leaflet.esm.js", | ||
"types": "dist/sigma-layer-leaflet.cjs.d.ts", | ||
"files": [ | ||
"/dist" | ||
], | ||
"sideEffects": false, | ||
"homepage": "https://www.sigmajs.org", | ||
"bugs": "http://github.com/jacomyal/sigma.js/issues", | ||
"repository": { | ||
"type": "git", | ||
"url": "http://github.com/jacomyal/sigma.js.git" | ||
}, | ||
"keywords": [ | ||
"graph", | ||
"graphology", | ||
"sigma" | ||
], | ||
"license": "MIT", | ||
"preconstruct": { | ||
"entrypoints": [ | ||
"index.ts" | ||
] | ||
}, | ||
"peerDependencies": { | ||
"leaflet": "^1.9.4", | ||
"sigma": ">=3.0.0-beta.10" | ||
}, | ||
"exports": { | ||
".": { | ||
"module": "./dist/sigma-layer-leaflet.esm.js", | ||
"import": "./dist/sigma-layer-leaflet.cjs.mjs", | ||
"default": "./dist/sigma-layer-leaflet.cjs.js" | ||
}, | ||
"./package.json": "./package.json" | ||
}, | ||
"devDependencies": { | ||
"@types/leaflet": "^1.9.12" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
import Graph from "graphology"; | ||
import { Attributes } from "graphology-types"; | ||
import L from "leaflet"; | ||
import { Sigma } from "sigma"; | ||
import { DEFAULT_SETTINGS } from "sigma/settings"; | ||
|
||
import { graphToLatlng, latlngToGraph, setSigmaRatioBounds, syncLeafletBboxWithGraph } from "./utils"; | ||
|
||
/** | ||
* On the graph, we store the 2D projection of the geographical lat/long. | ||
*/ | ||
export default function bindLeafletLayer( | ||
sigma: Sigma, | ||
opts?: { | ||
tileLayer?: { urlTemplate: string; attribution?: string }; | ||
getNodeLatLng?: (nodeAttributes: Attributes) => { lat: number; lng: number }; | ||
}, | ||
) { | ||
// Creating map container for leaflet | ||
const mapContainer = document.createElement("div"); | ||
const mapContainerId = `${sigma.getContainer().id}-map`; | ||
mapContainer.setAttribute("id", mapContainerId); | ||
mapContainer.setAttribute("style", "position: relative; top:0; left:0; width: 100%; height:100%; z-index:-1"); | ||
sigma.getContainer().appendChild(mapContainer); | ||
|
||
// Initialize the map | ||
const map = L.map(mapContainerId, { | ||
zoomControl: false, | ||
zoomSnap: 0, | ||
zoom: 0, | ||
// we force the maxZoom with a higher tile value so leaflet function are not stucks | ||
// in a restricted area. It avoids side effect | ||
maxZoom: 20, | ||
}).setView([0, 0], 0); | ||
let tileUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; | ||
let tileAttribution: string | undefined = | ||
'© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'; | ||
if (opts?.tileLayer) { | ||
tileUrl = opts.tileLayer.urlTemplate; | ||
tileAttribution = opts.tileLayer.attribution; | ||
} | ||
L.tileLayer(tileUrl, { attribution: tileAttribution }).addTo(map); | ||
|
||
// `stagePadding: 0` is mandatory, so the bbox of the map & Sigma is the same. | ||
sigma.setSetting("stagePadding", 0); | ||
|
||
// Function that change the given graph by generating the sigma x,y coords by taking the geo coordinates | ||
// and project them in the 2D space of the map | ||
function updateGraphCoordinates(graph: Graph) { | ||
graph.updateEachNodeAttributes((_node, attrs) => { | ||
const coords = latlngToGraph( | ||
map, | ||
opts?.getNodeLatLng ? opts.getNodeLatLng(attrs) : { lat: attrs.lat, lng: attrs.lng }, | ||
); | ||
return { | ||
...attrs, | ||
x: coords.x, | ||
y: coords.y, | ||
}; | ||
}); | ||
} | ||
|
||
// Function that do sync sigma->leaflet | ||
function fnSyncLeaflet(animate = false) { | ||
syncLeafletBboxWithGraph(sigma, map, animate); | ||
} | ||
|
||
// When sigma is resize, we need to update the graph coordinate (the ref has changed) | ||
// and recompute the zoom bounds | ||
function fnOnResize() { | ||
updateGraphCoordinates(sigma.getGraph()); | ||
setSigmaRatioBounds(sigma, map); | ||
} | ||
|
||
// Clean up function to remove everything | ||
function clean() { | ||
map.remove(); | ||
mapContainer.remove(); | ||
sigma.off("afterRender", fnSyncLeaflet); | ||
sigma.off("resize", fnOnResize); | ||
sigma.setSetting("stagePadding", DEFAULT_SETTINGS.stagePadding); | ||
} | ||
|
||
// WHen the map is ready | ||
map.whenReady(() => { | ||
// Update the sigma graph for geopspatial coords | ||
updateGraphCoordinates(sigma.getGraph()); | ||
|
||
// Do the first sync | ||
fnSyncLeaflet(); | ||
|
||
// Compute sigma ratio bounds | ||
map.once("moveend", () => { | ||
setSigmaRatioBounds(sigma, map); | ||
}); | ||
|
||
// At each render of sigma, we do the leaflet sync | ||
sigma.on("afterRender", fnSyncLeaflet); | ||
// Listen on resize | ||
sigma.on("resize", fnOnResize); | ||
// Do the cleanup | ||
sigma.on("kill", clean); | ||
}); | ||
|
||
return { | ||
clean, | ||
map, | ||
updateGraphCoordinates, | ||
}; | ||
} | ||
|
||
export { graphToLatlng, latlngToGraph }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,77 @@ | ||
import { LatLngBounds, Map } from "leaflet"; | ||
import { Sigma } from "sigma"; | ||
|
||
export const LEAFLET_MAX_PIXEL = 256 * 2 ** 18; | ||
|
||
/** | ||
* Given a geo point returns its graph coords. | ||
*/ | ||
export function latlngToGraph(map: Map, coord: { lat: number; lng: number }): { x: number; y: number } { | ||
const data = map.project({ lat: coord.lat, lng: coord.lng }, 0); | ||
return { | ||
x: data.x, | ||
// Y are reversed between geo / sigma | ||
y: map.getContainer().clientHeight - data.y, | ||
}; | ||
} | ||
|
||
/** | ||
* Given a graph coords returns it's lat/lng coords. | ||
*/ | ||
export function graphToLatlng(map: Map, coords: { x: number; y: number }): { lat: number; lng: number } { | ||
const data = map.unproject([coords.x, map.getContainer().clientHeight - coords.y], 0); | ||
return { lat: data.lat, lng: data.lng }; | ||
} | ||
|
||
/** | ||
* Synchronise the sigma BBOX with the leaflet one. | ||
*/ | ||
export function syncLeafletBboxWithGraph(sigma: Sigma, map: Map, animate: boolean): void { | ||
const viewportDimensions = sigma.getDimensions(); | ||
|
||
// Graph BBOX | ||
const graphBottomLeft = sigma.viewportToGraph({ x: 0, y: viewportDimensions.height }, { padding: 0 }); | ||
const graphTopRight = sigma.viewportToGraph({ x: viewportDimensions.width, y: 0 }, { padding: 0 }); | ||
|
||
// Geo BBOX | ||
const geoSouthWest = graphToLatlng(map, graphBottomLeft); | ||
const geoNorthEast = graphToLatlng(map, graphTopRight); | ||
|
||
// Set map BBOX | ||
const bounds = new LatLngBounds(geoSouthWest, geoNorthEast); | ||
const opts = animate ? { animate: true, duration: 0.001 } : { animate: false }; | ||
map.flyToBounds(bounds, opts); | ||
|
||
// Handle side effects when bounds have some "void" area on top or bottom of the map | ||
// When it happens, flyToBound don't really do its job and there is a translation of the graph that match the void height. | ||
// So we have to do a pan in pixel... | ||
const worldSize = map.getPixelWorldBounds().getSize(); | ||
const mapBottomY = map.getPixelBounds().getBottomLeft().y; | ||
const mapTopY = map.getPixelBounds().getTopRight().y; | ||
const panVector: [number, number] = [0, 0]; | ||
if (mapTopY < 0) panVector[1] = mapTopY; | ||
if (mapBottomY > worldSize.y) panVector[1] = mapBottomY - worldSize.y + panVector[1]; | ||
if (panVector[1] !== 0) { | ||
map.panBy(panVector, { animate: false }); | ||
} | ||
} | ||
|
||
/** | ||
* Settings the min & max camera ratio of sigma to not be hover the possibility of leaflet | ||
* - Max zoom is when whe can see the whole map | ||
* - Min zoom is when we are at zoom 18 on leaflet | ||
*/ | ||
export function setSigmaRatioBounds(sigma: Sigma, map: Map): void { | ||
const worldPixelSize = map.getPixelWorldBounds().getSize(); | ||
|
||
// Max zoom | ||
const maxZoomRatio = worldPixelSize.y / sigma.getDimensions().width; | ||
sigma.setSetting("maxCameraRatio", maxZoomRatio); | ||
// Min zoom | ||
const minZoomRatio = worldPixelSize.y / LEAFLET_MAX_PIXEL; | ||
sigma.setSetting("minCameraRatio", minZoomRatio); | ||
|
||
const currentRatio = sigma.getCamera().ratio; | ||
if (currentRatio > maxZoomRatio) sigma.getCamera().setState({ ratio: maxZoomRatio }); | ||
if (currentRatio < minZoomRatio) sigma.getCamera().setState({ ratio: minZoomRatio }); | ||
} |
Oops, something went wrong.