Skip to content

Commit 96afbb7

Browse files
committed
[gephi-lite] Improves map styles/tiles management
Details: - Replaces TileJSON URL with a full styles JSON object - Updates default styles to match Gephi Lite graphical style - Makes styles JSON editable using Monaco
1 parent 7e85871 commit 96afbb7

File tree

6 files changed

+196
-43
lines changed

6 files changed

+196
-43
lines changed

packages/gephi-lite/src/components/GraphAppearance/background/MapBackgroundLayerForm.tsx

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,67 @@
11
import { MapBackgroundLayer } from "@gephi/gephi-lite-sdk";
2-
import { FC, useMemo } from "react";
2+
import { FC } from "react";
33
import { useTranslation } from "react-i18next";
44

55
import { useAppearance, useAppearanceActions } from "../../../core/context/dataContexts";
6-
import { EnumInput, StringInput } from "../../forms/TypedInputs";
7-
8-
const ENGINES = ["maplibre"] as const;
6+
import { useModal } from "../../../core/modals";
7+
import { getDefaultMapStyle } from "../../../utils/map-style";
8+
import { CodeEditorIcon } from "../../common-icons";
9+
import { MapStyleEditorModal } from "./MapStyleEditorModal";
910

1011
export const MapBackgroundLayerForm: FC = () => {
1112
const { t } = useTranslation();
1213
const { backgroundLayer } = useAppearance();
1314
const { setBackgroundLayer } = useAppearanceActions();
15+
const { openModal } = useModal();
1416

1517
const mapLayer = backgroundLayer?.type === "map" ? backgroundLayer : null;
1618

17-
const engineOptions = useMemo(
18-
() => ENGINES.map((engine) => ({ value: engine, label: t(`appearance.background.map.${engine}.name`) })),
19-
[t],
20-
);
21-
2219
const setMapLayer = (updates: Partial<MapBackgroundLayer["map"]>) => {
2320
const current = mapLayer?.map || { engine: "maplibre" };
2421
setBackgroundLayer({ type: "map", map: { ...current, ...updates } });
2522
};
2623

24+
const currentStyle = mapLayer?.map.style;
25+
const styleName = (currentStyle?.name as string) || t("appearance.background.map.maplibre.custom_style");
26+
2727
return (
2828
<div className="panel-block">
2929
<h3>{t("appearance.background.map.title")}</h3>
3030

3131
<div className="panel-block">
32-
<EnumInput
33-
id="map-engine"
34-
label={t("appearance.background.map.engine")}
35-
value={mapLayer?.map.engine || "maplibre"}
36-
options={engineOptions}
37-
onChange={(v) => setMapLayer({ engine: (v as "maplibre") || "maplibre" })}
38-
required
39-
/>
40-
</div>
41-
42-
<div className="panel-block">
43-
<StringInput
44-
id="map-tiles"
45-
label={t("appearance.background.map.maplibre.tiles")}
46-
value={mapLayer?.map.tiles || null}
47-
onChange={(v) => setMapLayer({ tiles: v || undefined })}
48-
/>
32+
<label className="form-label">{t("appearance.background.map.maplibre.style")}</label>
33+
<div className="d-flex align-items-center gl-gap-2">
34+
<span className="flex-grow-1 text-ellipsis">
35+
{currentStyle ? styleName : t("appearance.background.map.maplibre.style_default")}
36+
</span>
37+
<button
38+
type="button"
39+
className="gl-btn gl-btn-outline gl-btn-sm"
40+
title={t("appearance.background.map.maplibre.edit_style")}
41+
onClick={() =>
42+
openModal({
43+
component: MapStyleEditorModal,
44+
arguments: {
45+
initialStyle: JSON.stringify(currentStyle || getDefaultMapStyle(), null, 2),
46+
},
47+
beforeSubmit: ({ style }) => setMapLayer({ style }),
48+
})
49+
}
50+
>
51+
<CodeEditorIcon className="me-1" />
52+
{t("common.edit")}
53+
</button>
54+
{currentStyle && (
55+
<button
56+
type="button"
57+
className="gl-btn gl-btn-outline gl-btn-sm"
58+
title={t("appearance.background.map.maplibre.reset_style")}
59+
onClick={() => setMapLayer({ style: undefined })}
60+
>
61+
{t("common.reset")}
62+
</button>
63+
)}
64+
</div>
4965
</div>
5066
</div>
5167
);
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import Editor from "@monaco-editor/react";
2+
import { FC, useState } from "react";
3+
import { useTranslation } from "react-i18next";
4+
5+
import { usePreferences } from "../../../core/context/dataContexts";
6+
import { ModalProps } from "../../../core/modals/types";
7+
import { getAppliedTheme } from "../../../core/preferences/utils";
8+
import { Modal } from "../../modals";
9+
10+
export const MapStyleEditorModal: FC<
11+
ModalProps<{ initialStyle: string }, { style: Record<string, unknown> }>
12+
> = ({ arguments: { initialStyle }, cancel, submit }) => {
13+
const { t } = useTranslation();
14+
const { theme } = usePreferences();
15+
const [value, setValue] = useState(initialStyle);
16+
const [error, setError] = useState<string | null>(null);
17+
18+
const save = () => {
19+
try {
20+
const parsed = JSON.parse(value);
21+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
22+
setError("Style must be a JSON object");
23+
return;
24+
}
25+
submit({ style: parsed as Record<string, unknown> });
26+
} catch (e) {
27+
setError(`${e}`);
28+
}
29+
};
30+
31+
return (
32+
<Modal
33+
className="modal-xl"
34+
bodyClassName="p-0"
35+
title={t("appearance.background.map.maplibre.style_editor_title")}
36+
onClose={() => cancel()}
37+
onSubmit={save}
38+
>
39+
<>
40+
{error && (
41+
<div className="alert gl-m-0 gl-alert-error d-flex flex-column align-items-center mb-3">
42+
<p className="mb-0">{error}</p>
43+
</div>
44+
)}
45+
<Editor
46+
height="60vh"
47+
theme={getAppliedTheme(theme) === "light" ? "light" : "vs-dark"}
48+
language="json"
49+
value={value}
50+
onChange={(v) => {
51+
setError(null);
52+
setValue(v || "");
53+
}}
54+
options={{ tabSize: 2, minimap: { enabled: false } }}
55+
/>
56+
</>
57+
58+
<div className="gl-gap-2 d-flex">
59+
<button type="button" title={t("common.cancel")} className="gl-btn gl-btn-outline" onClick={() => cancel()}>
60+
{t("common.cancel")}
61+
</button>
62+
<button type="submit" title={t("common.save")} className="gl-btn gl-btn-fill">
63+
{t("common.save")}
64+
</button>
65+
</div>
66+
</Modal>
67+
);
68+
};

packages/gephi-lite/src/locales/en.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
"engine": "Engine:",
1010
"maplibre": {
1111
"name": "MapLibre",
12-
"tiles": "TileJSON URL"
12+
"style": "Map style:",
13+
"style_default": "Default style",
14+
"edit_style": "Edit map style",
15+
"reset_style": "Reset to default",
16+
"style_editor_title": "Map Style Editor",
17+
"custom_style": "Custom style"
1318
}
1419
}
1520
},
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
export const DEFAULT_MAP_STYLE = {
2+
version: 8,
3+
name: "Gephi Lite Default",
4+
sources: {
5+
demotiles: {
6+
type: "vector",
7+
url: "https://demotiles.maplibre.org/tiles/tiles.json",
8+
},
9+
},
10+
glyphs: "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
11+
layers: [
12+
{
13+
id: "background",
14+
type: "background",
15+
paint: { "background-color": "#f5f5f5" },
16+
},
17+
{
18+
id: "countries-fill",
19+
type: "fill",
20+
source: "demotiles",
21+
"source-layer": "countries",
22+
paint: { "fill-color": "#ffffff" },
23+
},
24+
{
25+
id: "countries-boundary",
26+
type: "line",
27+
source: "demotiles",
28+
"source-layer": "countries",
29+
paint: { "line-color": "#dee2e6", "line-width": 1 },
30+
},
31+
{
32+
id: "geolines",
33+
type: "line",
34+
source: "demotiles",
35+
"source-layer": "geolines",
36+
paint: { "line-color": "#dee2e6", "line-width": 1.5 },
37+
},
38+
{
39+
id: "country-labels",
40+
type: "symbol",
41+
source: "demotiles",
42+
"source-layer": "centroids",
43+
layout: {
44+
"text-field": "{NAME}",
45+
"text-font": ["Open Sans Semibold"],
46+
"text-size": 12,
47+
},
48+
paint: {
49+
"text-color": "#adb5bd",
50+
"text-halo-color": "#ffffff",
51+
"text-halo-width": 1.5,
52+
},
53+
},
54+
],
55+
} as const;
56+
57+
export function getDefaultMapStyle(): Record<string, unknown> {
58+
return JSON.parse(JSON.stringify(DEFAULT_MAP_STYLE));
59+
}

packages/gephi-lite/src/views/graphPage/controllers/MapLayerController.tsx

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,50 @@
11
import { useSigma } from "@react-sigma/core";
2-
import { LngLatBounds, Map, MercatorCoordinate } from "maplibre-gl";
2+
import { LngLatBounds, Map, MercatorCoordinate, StyleSpecification } from "maplibre-gl";
33
import { FC, useCallback, useEffect, useRef } from "react";
44

55
import { useAppearance } from "../../../core/context/dataContexts";
6+
import { getDefaultMapStyle } from "../../../utils/map-style";
67

78
// Convert graph coordinates (with Y-flip) to geo coordinates
89
function graphToLatlng(coords: { x: number; y: number }) {
910
const mercator = new MercatorCoordinate(coords.x, 1 - coords.y, 0);
1011
return mercator.toLngLat();
1112
}
1213

13-
const DEFAULT_TILES = "https://demotiles.maplibre.org/style.json";
14-
1514
export const MapLayerController: FC = () => {
1615
const sigma = useSigma();
1716
const { backgroundLayer } = useAppearance();
1817

1918
const mapRef = useRef<Map | null>(null);
2019
const containerRef = useRef<HTMLDivElement | null>(null);
2120
const lastCameraStateRef = useRef<string | null>(null);
21+
const sigmaRef = useRef(sigma);
22+
sigmaRef.current = sigma;
2223

2324
const mapConfig = backgroundLayer?.type === "map" ? backgroundLayer.map : null;
24-
const tilesUrl = mapConfig?.tiles || DEFAULT_TILES;
25+
const mapStyle = (mapConfig?.style || getDefaultMapStyle()) as StyleSpecification;
26+
const styleKey = JSON.stringify(mapStyle);
2527

2628
// Sync map bounds to match sigma's viewport (skips if camera hasn't moved)
2729
const syncMapFromSigma = useCallback(() => {
2830
const map = mapRef.current;
31+
const s = sigmaRef.current;
2932
if (!map) return;
3033

31-
const { x, y, ratio, angle } = sigma.getCamera().getState();
34+
const { x, y, ratio, angle } = s.getCamera().getState();
3235
const key = `${x},${y},${ratio},${angle}`;
3336
if (key === lastCameraStateRef.current) return;
3437
lastCameraStateRef.current = key;
3538

36-
const dims = sigma.getDimensions();
37-
const bottomLeft = sigma.viewportToGraph({ x: 0, y: dims.height }, { padding: 0 });
38-
const topRight = sigma.viewportToGraph({ x: dims.width, y: 0 }, { padding: 0 });
39+
const dims = s.getDimensions();
40+
const bottomLeft = s.viewportToGraph({ x: 0, y: dims.height }, { padding: 0 });
41+
const topRight = s.viewportToGraph({ x: dims.width, y: 0 }, { padding: 0 });
3942

4043
const southWest = graphToLatlng(bottomLeft);
4144
const northEast = graphToLatlng(topRight);
4245

4346
map.fitBounds(new LngLatBounds(southWest, northEast), { duration: 0 });
44-
}, [sigma]);
47+
}, []);
4548

4649
// Initialize or clean up map based on mapConfig
4750
const isMapMode = !!mapConfig;
@@ -59,7 +62,7 @@ export const MapLayerController: FC = () => {
5962
}
6063

6164
// Create container for MapLibre
62-
const sigmaContainer = sigma.getContainer();
65+
const sigmaContainer = sigmaRef.current.getContainer();
6366
if (!containerRef.current) {
6467
const container = document.createElement("div");
6568
container.style.position = "absolute";
@@ -71,9 +74,10 @@ export const MapLayerController: FC = () => {
7174
}
7275

7376
// Create MapLibre map
77+
const style = JSON.parse(styleKey) as StyleSpecification;
7478
const map = new Map({
7579
container: containerRef.current,
76-
style: tilesUrl,
80+
style,
7781
interactive: false,
7882
attributionControl: false,
7983
});
@@ -91,18 +95,19 @@ export const MapLayerController: FC = () => {
9195
containerRef.current = null;
9296
}
9397
};
94-
}, [sigma, isMapMode, tilesUrl, syncMapFromSigma]);
98+
}, [isMapMode, styleKey, syncMapFromSigma]);
9599

96100
// Sync map camera on sigma afterRender
97101
useEffect(() => {
98102
if (!isMapMode) return;
103+
const s = sigmaRef.current;
99104

100105
const handler = () => syncMapFromSigma();
101-
sigma.on("afterRender", handler);
106+
s.on("afterRender", handler);
102107
return () => {
103-
sigma.off("afterRender", handler);
108+
s.off("afterRender", handler);
104109
};
105-
}, [sigma, isMapMode, syncMapFromSigma]);
110+
}, [isMapMode, syncMapFromSigma]);
106111

107112
// Handle resize
108113
useEffect(() => {

packages/sdk/src/appearance/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ export type MapBackgroundLayer = {
103103
type: "map";
104104
map: {
105105
engine: "maplibre";
106-
tiles?: string;
106+
style?: Record<string, unknown>;
107107
// If undefined: direct mapping (x=lng, y=lat)
108108
// If defined: linear transform from graph extent to geo extent
109109
coordinateMapping?: MapCoordinateMapping;

0 commit comments

Comments
 (0)