Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Module/Components/VisualizationComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export class VisualizationComponent implements IComponentOptions
public controller = VisualizationComponentController;
public bindings = {
result: '=',
storageKey: '@',
};

}
258 changes: 228 additions & 30 deletions src/Module/Components/VisualizationComponentController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,188 @@ import {Strings} from '@src/Utils/Strings';
import model from '@src/Data/Model';
import {ProductionResult} from '@src/Tools/Production/Result/ProductionResult';

const DONE_NODE_BORDER = 'rgba(180, 255, 180, 0.9)';
const DONE_NODE_BORDER_HL = 'rgba(220, 255, 220, 1)';
const DONE_NODE_BORDER_WIDTH = 1;
const DONE_NODE_BG_ALPHA = 0.35;
const DONE_FONT_ALPHA = 0.45;

const DONE_EDGE_COLOR = 'rgba(105, 125, 145, 0.3)';
const DONE_EDGE_COLOR_HL = 'rgba(134, 151, 167, 0.45)';
const DONE_EDGE_FONT_COLOR = 'rgba(238, 238, 238, 0.3)';

const STORAGE_KEY_PREFIX = 'doneNodes_';

function fadeRgba(rgba: string, alpha: number): string
{
const m = rgba.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);

if (!m) {
return rgba;
}

return `rgba(${m[1]}, ${m[2]}, ${m[3]}, ${alpha})`;
}

export class VisualizationComponentController implements IController
{

public result: ProductionResult;

public storageKey: string;

public static $inject = ['$element', '$scope', '$timeout'];

private unregisterWatcherCallback: () => void;
private network: Network;
private fitted: boolean = false;

private doneKeys: Set<string> = new Set();

private visNodes: DataSet<IVisNode>;
private visEdges: DataSet<IVisEdge>;

private nodeStyles: Map<number, { stableKey: string; originalColor: any; originalFont: any }> = new Map();
private edgeStyles: Map<number, { stableKey: string; originalColor: any; originalFont: any }> = new Map();

public constructor(private readonly $element: any, private readonly $scope: IScope, private readonly $timeout: ITimeoutService) {}


public $onInit(): void
{
this.unregisterWatcherCallback = this.$scope.$watch(() => {
return this.result;
}, (newValue) => {
this.updateData(newValue);
});
return {
result: this.result,
storageKey: this.storageKey,
};
}, (newValue: { result: ProductionResult; storageKey: string }) => {
this.updateData(newValue.result, newValue.storageKey);
}, true);
}

public $onDestroy(): void
{
this.unregisterWatcherCallback();
}

private loadDoneKeys(key: string): void
{
try {
const raw = localStorage.getItem(STORAGE_KEY_PREFIX + key);

this.doneKeys = raw ? new Set(JSON.parse(raw)) : new Set();
} catch {
this.doneKeys = new Set();
}
}

private saveDoneKeys(): void
{
try {
const key = STORAGE_KEY_PREFIX + (this.storageKey || 'default');

localStorage.setItem(key, JSON.stringify([...this.doneKeys]));
} catch { /* storage full: silently ignore */ }
}

private applyDoneNodeStyle(id: number, isDone: boolean): void
{
const meta = this.nodeStyles.get(id);

if (!meta) {
return;
}

if (isDone) {
const orig = meta.originalColor;

this.visNodes.update({
id,
color: {
border: DONE_NODE_BORDER,
background: fadeRgba(orig.background, DONE_NODE_BG_ALPHA),
highlight: {
border: DONE_NODE_BORDER_HL,
background: fadeRgba(orig.highlight.background, DONE_NODE_BG_ALPHA),
},
},
borderWidth: DONE_NODE_BORDER_WIDTH,
borderWidthSelected: DONE_NODE_BORDER_WIDTH + 1,
font: {
color: fadeRgba(meta.originalFont.color, DONE_FONT_ALPHA),
},
} as any);
} else {
this.visNodes.update({
id,
color: meta.originalColor,
borderWidth: 0,
borderWidthSelected: 1,
font: meta.originalFont,
} as any);
}
}

private applyDoneEdgeStyle(id: number, isDone: boolean): void
{
const meta = this.edgeStyles.get(id);
if (!meta) { return; }
if (isDone) {
this.visEdges.update({
id,
color: {
color: DONE_EDGE_COLOR,
highlight: DONE_EDGE_COLOR_HL,
},
font: {
color: DONE_EDGE_FONT_COLOR,
},
dashes: true,
} as any);
} else {
this.visEdges.update({
id,
color: meta.originalColor,
font: meta.originalFont,
dashes: false,
} as any);
}
}

private toggleNodeDone(visId: number): void
{
const meta = this.nodeStyles.get(visId);

if (!meta) {
return;
}

const key = meta.stableKey;
const isDone = !this.doneKeys.has(key);

isDone ? this.doneKeys.add(key) : this.doneKeys.delete(key);

this.applyDoneNodeStyle(visId, isDone);
this.saveDoneKeys();
}

private toggleEdgeDone(visId: number): void
{
const meta = this.edgeStyles.get(visId);

if (!meta) {
return;
}

const key = meta.stableKey;
const isDone = !this.doneKeys.has(key);

isDone ? this.doneKeys.add(key) : this.doneKeys.delete(key);

this.applyDoneEdgeStyle(visId, isDone);
this.saveDoneKeys();
}

public useCytoscape(result: ProductionResult): void
{
const options: cytoscape.CytoscapeOptions = {
Expand Down Expand Up @@ -106,13 +260,28 @@ export class VisualizationComponentController implements IController
const cy = cytoscape(options as any);
}

public useVis(result: ProductionResult): void
public useVis(result: ProductionResult, storageKey: string): void
{
const nodes = new DataSet<IVisNode>();
const edges = new DataSet<IVisEdge>();
this.storageKey = storageKey;
this.loadDoneKeys(storageKey);

this.nodeStyles.clear();
this.edgeStyles.clear();

this.visNodes = new DataSet<IVisNode>();
this.visEdges = new DataSet<IVisEdge>();

for (const node of result.graph.nodes) {
nodes.add(node.getVisNode());
const visNode = node.getVisNode();

this.visNodes.add(visNode);

// Store original style + stable key for later toggling.
this.nodeStyles.set(node.id, {
stableKey: node.getStableKey(),
originalColor: visNode.color ? { ...visNode.color, highlight: { ...visNode.color.highlight } } : undefined,
originalFont: visNode.font ? { ...visNode.font } : { color: 'rgba(238, 238, 238, 1)' },
});
}

for (const edge of result.graph.edges) {
Expand All @@ -122,48 +291,77 @@ export class VisualizationComponentController implements IController

if (edge.to.hasOutputTo(edge.from)) {
smooth.enabled = true;
smooth.type = 'curvedCW'
smooth.type = 'curvedCW';
smooth.roundness = 0.2;
}

edges.add({
id: edge.id,
from: edge.from.id,
to: edge.to.id,
const edgeColor = {
color: 'rgba(105, 125, 145, 1)',
highlight: 'rgba(134, 151, 167, 1)',
};
const edgeFont = {
color: 'rgba(238, 238, 238, 1)',
};

this.visEdges.add({
id: edge.id,
from: edge.from.id,
to: edge.to.id,
label: model.getItem(edge.itemAmount.item).prototype.name + '\n' + Strings.formatNumber(edge.itemAmount.amount) + ' / min',
color: {
color: 'rgba(105, 125, 145, 1)',
highlight: 'rgba(134, 151, 167, 1)',
},
font: {
color: 'rgba(238, 238, 238, 1)',
},
smooth: smooth,
color: edgeColor,
font: edgeFont,
smooth,
} as any);

this.edgeStyles.set(edge.id, {
stableKey: edge.getStableKey(),
originalColor: { ...edgeColor },
originalFont: { ...edgeFont },
});
}

this.network = this.drawVisualisation(nodes, edges);
// Apply persisted done state before the network is drawn.
this.nodeStyles.forEach(({ stableKey }, visId) => {
if (this.doneKeys.has(stableKey)) {
this.applyDoneNodeStyle(visId, true);
}
});
this.edgeStyles.forEach(({ stableKey }, visId) => {
if (this.doneKeys.has(stableKey)) {
this.applyDoneEdgeStyle(visId, true);
}
});

this.network = this.drawVisualisation(this.visNodes, this.visEdges);

this.network.on('doubleClick', (params) => {
if (params.nodes && params.nodes.length > 0) {
this.toggleNodeDone(params.nodes[0]);
} else if (params.edges && params.edges.length > 0) {
this.toggleEdgeDone(params.edges[0]);
}
});

this.$timeout(0).then(() => {
const elkGraph: IElkGraph = {
id: 'root',
layoutOptions: {
'elk.algorithm': 'org.eclipse.elk.layered',
'org.eclipse.elk.layered.nodePlacement.favorStraightEdges': true as unknown as string, // fuck off typescript
'org.eclipse.elk.layered.nodePlacement.favorStraightEdges': true as unknown as string,
'org.eclipse.elk.spacing.nodeNode': 40 + '',
},
children: [],
edges: [],
};

nodes.forEach((node) => {
this.visNodes.forEach((node) => {
elkGraph.children.push({
id: node.id.toString(),
width: 250,
height: 100,
});
});
edges.forEach((edge) => {
this.visEdges.forEach((edge) => {
elkGraph.edges.push({
id: '',
source: edge.from.toString(),
Expand All @@ -174,12 +372,12 @@ export class VisualizationComponentController implements IController
this.$timeout(0).then(() => {
const elk = new ELK();
elk.layout(elkGraph).then((data) => {
nodes.forEach((node) => {
this.visNodes.forEach((node) => {
const id = node.id;
if (data.children) {
for (const item of data.children) {
if (parseInt(item.id, 10) === id) {
nodes.update({
this.visNodes.update({
id: id,
x: item.x,
y: item.y,
Expand All @@ -199,21 +397,22 @@ export class VisualizationComponentController implements IController
});
}

public updateData(result: ProductionResult|undefined): void
public updateData(result: ProductionResult|undefined, storageKey?: string): void
{
if (!result) {
return;
}

this.fitted = false;

const key = storageKey || this.storageKey || 'default';
let use;
use = 'vis';

if (use === 'cytoscape') {
this.useCytoscape(result);
} else {
this.useVis(result);
this.useVis(result, key);
}
}

Expand All @@ -236,7 +435,6 @@ export class VisualizationComponentController implements IController
nodes: {
labelHighlightBold: false,
font: {
// align: 'left',
size: 14,
multi: 'html',
},
Expand Down
1 change: 1 addition & 0 deletions src/Tools/Production/IProductionData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface IProductionDataMetadata
icon: string|null;
schemaVersion: number;
gameVersion: string;
tabId?: string;

}

Expand Down
Loading