Skip to content

Commit

Permalink
[@sigma/layer-leaflet] WIP - first working version
Browse files Browse the repository at this point in the history
  • Loading branch information
sim51 committed Jul 18, 2024
1 parent 3394765 commit 1dcc1c5
Show file tree
Hide file tree
Showing 14 changed files with 322,797 additions and 3 deletions.
54 changes: 52 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"scripts": {
"clean": "npm exec --workspaces -- npx rimraf node_modules && npx rimraf node_modules",
"build": "preconstruct build && npm run build-bundle --workspace=sigma",
"preconstruct": "preconstruct",
"prettify": "prettier --write .",
"lint": "eslint .",
"test": "npm run test --workspace=@sigma/test",
Expand Down Expand Up @@ -40,7 +41,8 @@
"packages/node-border",
"packages/node-image",
"packages/node-piechart",
"packages/edge-curve"
"packages/edge-curve",
"packages/layer-leaflet"
],
"exports": {
"importConditionDefaultExport": "default"
Expand Down
2 changes: 2 additions & 0 deletions packages/layer-leaflet/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
dist
4 changes: 4 additions & 0 deletions packages/layer-leaflet/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.gitignore
node_modules
src
tsconfig.json
Empty file.
44 changes: 44 additions & 0 deletions packages/layer-leaflet/package.json
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"
}
}
129 changes: 129 additions & 0 deletions packages/layer-leaflet/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import Graph from "graphology";
import { Attributes } from "graphology-types";
import L, { LatLngBounds, Map } from "leaflet";
import { Sigma } from "sigma";
import { DEFAULT_SETTINGS } from "sigma/settings";

import { graphToLatlng, latlngToGraph } from "./utils";

/**
* Synchronise the sigma BBOX with the leaflet one.
*/
function syncLeafletBboxWithGraph(sigma: Sigma, map: Map): 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);
map.flyToBounds(bounds, { animate: false });

// 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;
if (panVector[1] !== 0) {
map.panBy(panVector, { animate: false });
}
}

/**
* On the graph, we store the 2D projection of the geographical lat/long.
*/
export function bindLeafletLayer(
sigma: Sigma,
opts?: {
tileLayer?: { urlTemplate: string; attribution?: string };
getNodeLatLng?: (nodeAttributes: Attributes) => { lat: number; lng: number };
},
) {
// Map initialization
// ~~~~~~~~~~~~~~~
// 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,
}).setView([0, 0], 0);
let tileUrl = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png";
let tileAttribution: string | undefined =
'&copy; <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);

// Graph mutation for geo
// ~~~~~~~~~~~~~~~~~~~~~~
// Mute 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) {
// const japan = latlngToGraph(map, { lat: 0, lng: 138 });
// console.log(japan);
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,
};
});
}
// Update the sigma graph for geopspatial coords
updateGraphCoordinates(sigma.getGraph());

// Sigma configuration & lifecycle
// ~~~~~~~~~~~~~~~
// `stagePadding: 0` is mandatory, so the bbox of the map & Sigma is the same.
sigma.setSetting("stagePadding", 0);
// Sync the graph BBOX with the leaflet one after each render.
function fnAfterRender() {
syncLeafletBboxWithGraph(sigma, map);
}
sigma.on("afterRender", fnAfterRender);

// Settings the max camera ratio to avoid side effect
// NB: Need to wait the first render of the map
setTimeout(() => {
const worldPixelSize = map.getPixelWorldBounds().getSize();
const mapPixelSize = map.getPixelBounds().getSize();
sigma.setSetting("maxCameraRatio", worldPixelSize.y / mapPixelSize.y);
}, 1000);

// Clean up function to remove everything
function clean() {
map.remove();
mapContainer.remove();
sigma.off("afterRender", fnAfterRender);
sigma.setSetting("stagePadding", DEFAULT_SETTINGS.stagePadding);
}
// When sigma is killed, do the cleanup
sigma.on("kill", clean);

return {
clean,
map,
updateGraphCoordinates,
};
}
21 changes: 21 additions & 0 deletions packages/layer-leaflet/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Map } from "leaflet";

/**
* 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 };
}
26 changes: 26 additions & 0 deletions packages/layer-leaflet/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ESNext", // Specifies the JavaScript version to target when transpiling code.
"useDefineForClassFields": true, // Enables the use of 'define' for class fields.
"lib": ["ES2020", "DOM", "DOM.Iterable"], // Specifies the libraries available for the code.
"module": "ESNext", // Defines the module system to use for code generation.
"skipLibCheck": true, // Skips type checking of declaration files.

/* Bundler mode */
"moduleResolution": "node", // Specifies how modules are resolved when bundling.
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true, // Allows importing TypeScript files with extensions.
"resolveJsonModule": true, // Enables importing JSON modules.
"isolatedModules": true, // Ensures each file is treated as a separate module.
"noEmit": true, // Prevents TypeScript from emitting output files.

/* Linting */
"strict": true, // Enables strict type checking.
"noUnusedLocals": true, // Flags unused local variables.
"noUnusedParameters": true, // Flags unused function parameters.
"noFallthroughCasesInSwitch": true, // Requires handling all cases in a switch statement.
"declaration": true // Generates declaration files for TypeScript.
},
"include": ["src"], // Specifies the directory to include when searching for TypeScript files.
"exclude": ["src/**/__docs__", "src/**/__test__"]
}
Loading

0 comments on commit 1dcc1c5

Please sign in to comment.