Skip to content

Commit fbe0894

Browse files
authored
feat: save graph on localStorage (#46)
Using [`localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) we can save the contents of the graph between restarts. NOTE: this implementation has concurrency issues when multiple tabs are open.
1 parent 2208b0f commit fbe0894

File tree

6 files changed

+160
-150
lines changed

6 files changed

+160
-150
lines changed

src/index.ejs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<body>
1111
<div id="top-bar">
12+
<button id="new-button" class="new-button" title="New">New</button>
1213
<button id="save-button" class="save-button" title="Save">Save</button>
1314
<button id="load-button" class="load-button" title="Load">Load</button>
1415
<button id="pause-button" class="pause-button" title="Pause"></button>

src/index.ts

Lines changed: 40 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ import {
1616
AddPc,
1717
AddRouter,
1818
AddServer,
19-
loadGraph,
20-
saveGraph,
19+
loadFromFile,
20+
loadFromLocalStorage,
21+
saveToFile,
22+
saveToLocalStorage,
2123
selectElement,
2224
} from "./types/viewportManager";
2325
import { DataGraph } from "./types/graphs/datagraph";
@@ -39,6 +41,12 @@ export class GlobalContext {
3941
this.viewgraph = new ViewGraph(this.datagraph, this.viewport);
4042
}
4143

44+
load(datagraph: DataGraph) {
45+
this.datagraph = datagraph;
46+
this.viewport.clear();
47+
this.viewgraph = new ViewGraph(this.datagraph, this.viewport);
48+
}
49+
4250
getViewport() {
4351
return this.viewport;
4452
}
@@ -90,6 +98,12 @@ export class Viewport extends pixi_viewport.Viewport {
9098
});
9199
}
92100

101+
clear() {
102+
this.removeChildren();
103+
this.addChild(new Background());
104+
this.moveCenter(WORLD_WIDTH / 2, WORLD_HEIGHT / 2);
105+
}
106+
93107
private initializeMovement() {
94108
this.drag()
95109
.pinch()
@@ -298,31 +312,13 @@ export class RightBar {
298312
RightBar.getInstance();
299313

300314
// Add router button
301-
leftBar.addButton(
302-
RouterSvg,
303-
() => {
304-
AddRouter(ctx);
305-
},
306-
"Add Router",
307-
);
315+
leftBar.addButton(RouterSvg, () => AddRouter(ctx), "Add Router");
308316

309317
// Add server button
310-
leftBar.addButton(
311-
ServerSvg,
312-
() => {
313-
AddServer(ctx);
314-
},
315-
"Add Server",
316-
);
318+
leftBar.addButton(ServerSvg, () => AddServer(ctx), "Add Server");
317319

318320
// Add PC button
319-
leftBar.addButton(
320-
ComputerSvg,
321-
() => {
322-
AddPc(ctx);
323-
},
324-
"Add PC",
325-
);
321+
leftBar.addButton(ComputerSvg, () => AddPc(ctx), "Add PC");
326322

327323
ctx.initialize(viewport);
328324

@@ -346,31 +342,13 @@ export class RightBar {
346342

347343
window.addEventListener("resize", resize);
348344

345+
const newButton = document.getElementById("new-button");
349346
const loadButton = document.getElementById("load-button");
350347
const saveButton = document.getElementById("save-button");
351348

352-
saveButton.onclick = () => {
353-
saveGraph(ctx);
354-
};
355-
356-
loadButton.onclick = () => {
357-
const input = document.createElement("input");
358-
input.type = "file";
359-
input.accept = ".json";
360-
361-
input.onchange = (event) => {
362-
const file = (event.target as HTMLInputElement).files[0];
363-
const reader = new FileReader();
364-
reader.readAsText(file);
365-
366-
reader.onload = (readerEvent) => {
367-
const jsonData = readerEvent.target.result as string;
368-
loadGraph(jsonData, ctx);
369-
};
370-
};
371-
372-
input.click();
373-
};
349+
newButton.onclick = () => ctx.load(new DataGraph());
350+
saveButton.onclick = () => saveToFile(ctx);
351+
loadButton.onclick = () => loadFromFile(ctx);
374352

375353
const pauseButton = document.getElementById("pause-button");
376354
let paused = false;
@@ -397,9 +375,7 @@ export class RightBar {
397375
}
398376
};
399377

400-
pauseButton.onclick = () => {
401-
triggerPause();
402-
};
378+
pauseButton.onclick = triggerPause;
403379

404380
document.body.onkeyup = function (e) {
405381
if (e.key === " " || e.code === "Space") {
@@ -408,5 +384,21 @@ export class RightBar {
408384
}
409385
};
410386

387+
// TODO: load from local storage directly, without first generating a context
388+
loadFromLocalStorage(ctx);
389+
390+
let saveIntervalId: NodeJS.Timeout | null = null;
391+
392+
ctx.getDataGraph().subscribeChanges(() => {
393+
// Wait a bit after the last change to save
394+
if (saveIntervalId) {
395+
clearInterval(saveIntervalId);
396+
}
397+
saveIntervalId = setInterval(() => {
398+
saveToLocalStorage(ctx);
399+
clearInterval(saveIntervalId);
400+
}, 100);
401+
});
402+
411403
console.log("initialized!");
412404
})();

src/style.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ canvas {
104104
}
105105

106106
/* Styles for save and load buttons */
107+
.new-button,
107108
.save-button,
108109
.load-button {
109110
background-color: #007bff; /* Blue background */

src/types/graphs/datagraph.ts

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,53 @@ export interface GraphNode {
55
connections: Set<number>;
66
}
77

8+
export interface GraphDataNode {
9+
id: number;
10+
x: number;
11+
y: number;
12+
type: string;
13+
connections: number[];
14+
}
15+
16+
export type GraphData = GraphDataNode[];
17+
818
export class DataGraph {
919
private devices = new Map<number, GraphNode>();
1020
private idCounter = 1;
21+
private onChanges: (() => void)[] = [];
22+
23+
static fromData(data: GraphData): DataGraph {
24+
const dataGraph = new DataGraph();
25+
data.forEach((nodeData: GraphDataNode) => {
26+
// ADD DATAGRAPH AND EDGES
27+
console.log(nodeData);
28+
const connections = new Set(nodeData.connections);
29+
const graphNode: GraphNode = {
30+
x: nodeData.x,
31+
y: nodeData.y,
32+
type: nodeData.type,
33+
connections: connections,
34+
};
35+
dataGraph.addDevice(nodeData.id, graphNode);
36+
});
37+
return dataGraph;
38+
}
39+
40+
toData(): GraphData {
41+
const graphData: GraphData = [];
42+
43+
// Serialize nodes
44+
this.getDevices().forEach(([id, info]) => {
45+
graphData.push({
46+
id: id,
47+
x: info.x,
48+
y: info.y,
49+
type: info.type, // Save the device type (Router, Server, PC)
50+
connections: Array.from(info.connections.values()),
51+
});
52+
});
53+
return graphData;
54+
}
1155

1256
// Add a new device to the graph
1357
addNewDevice(deviceInfo: { x: number; y: number; type: string }): number {
@@ -18,20 +62,22 @@ export class DataGraph {
1862
};
1963
this.devices.set(id, graphnode);
2064
console.log(`Device added with ID ${id}`);
65+
this.notifyChanges();
2166
return id;
2267
}
2368

2469
// Add a device to the graph
2570
addDevice(idDevice: number, deviceInfo: GraphNode) {
26-
if (!this.devices.has(idDevice)) {
27-
this.devices.set(idDevice, deviceInfo);
28-
if (this.idCounter <= idDevice) {
29-
this.idCounter = idDevice + 1;
30-
}
31-
console.log(`Device added with ID ${idDevice}`);
32-
} else {
71+
if (this.devices.has(idDevice)) {
3372
console.warn(`Device with ID ${idDevice} already exists in the graph.`);
73+
return;
74+
}
75+
this.devices.set(idDevice, deviceInfo);
76+
if (this.idCounter <= idDevice) {
77+
this.idCounter = idDevice + 1;
3478
}
79+
console.log(`Device added with ID ${idDevice}`);
80+
this.notifyChanges();
3581
}
3682

3783
// Add a connection between two devices
@@ -40,23 +86,30 @@ export class DataGraph {
4086
console.warn(
4187
`Cannot create a connection between the same device (ID ${n1Id}).`,
4288
);
43-
} else if (!this.devices.has(n1Id)) {
89+
return;
90+
}
91+
if (!this.devices.has(n1Id)) {
4492
console.warn(`Device with ID ${n1Id} does not exist in devices.`);
45-
} else if (!this.devices.has(n2Id)) {
93+
return;
94+
}
95+
if (!this.devices.has(n2Id)) {
4696
console.warn(`Device with ID ${n2Id} does not exist in devices.`);
97+
return;
4798
// Check if an edge already exists between these two devices
48-
} else if (this.devices.get(n1Id).connections.has(n2Id)) {
99+
}
100+
if (this.devices.get(n1Id).connections.has(n2Id)) {
49101
console.warn(
50102
`Connection between ID ${n1Id} and ID ${n2Id} already exists.`,
51103
);
52-
} else {
53-
this.devices.get(n1Id).connections.add(n2Id);
54-
this.devices.get(n2Id).connections.add(n1Id);
55-
56-
console.log(
57-
`Connection created between devices ID: ${n1Id} and ID: ${n2Id}`,
58-
);
104+
return;
59105
}
106+
this.devices.get(n1Id).connections.add(n2Id);
107+
this.devices.get(n2Id).connections.add(n1Id);
108+
109+
console.log(
110+
`Connection created between devices ID: ${n1Id} and ID: ${n2Id}`,
111+
);
112+
this.notifyChanges();
60113
}
61114

62115
updateDevicePosition(id: number, newValues: { x?: number; y?: number }) {
@@ -66,6 +119,7 @@ export class DataGraph {
66119
return;
67120
}
68121
this.devices.set(id, { ...deviceGraphNode, ...newValues });
122+
this.notifyChanges();
69123
}
70124

71125
getDevice(id: number): GraphNode | undefined {
@@ -111,6 +165,7 @@ export class DataGraph {
111165
// Remove the node from the graph
112166
this.devices.delete(id);
113167
console.log(`Device with ID ${id} and its connections were removed.`);
168+
this.notifyChanges();
114169
}
115170

116171
// Method to remove a connection (edge) between two devices by their IDs
@@ -143,11 +198,14 @@ export class DataGraph {
143198
console.log(
144199
`Connection removed between devices ID: ${n1Id} and ID: ${n2Id}`,
145200
);
201+
this.notifyChanges();
202+
}
203+
204+
subscribeChanges(callback: () => void) {
205+
this.onChanges.push(callback);
146206
}
147207

148-
// Clear the graph
149-
clear() {
150-
this.devices.clear();
151-
this.idCounter = 1;
208+
notifyChanges() {
209+
this.onChanges.forEach((callback) => callback());
152210
}
153211
}

src/types/graphs/viewgraph.ts

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class ViewGraph {
1616
this.constructView();
1717
}
1818

19-
constructView() {
19+
private constructView() {
2020
// TODO: Adjust construction based on the selected layer in the future
2121
console.log("Constructing ViewGraph from DataGraph");
2222
const connections = new Set<{ deviceId: number; adyacentId: number }>();
@@ -180,17 +180,6 @@ export class ViewGraph {
180180
return this.devices.size;
181181
}
182182

183-
// Clear the graph
184-
clear() {
185-
this.devices.forEach((device) => {
186-
device.delete();
187-
});
188-
// no edges should remain to delete
189-
this.devices.clear();
190-
this.edges.clear();
191-
this.idCounter = 1;
192-
}
193-
194183
// Method to remove a device and its connections (edges)
195184
removeDevice(id: number) {
196185
const device = this.devices.get(id);

0 commit comments

Comments
 (0)