Files
GitJournal/lib/core/graph.dart
Vishesh Handa 5571fdb65e Use time.dart
I'm sacrificing const code for readability. So maybe I'll lose a bit of
performance, but this is so much easier to read.
2020-11-10 19:07:09 +01:00

357 lines
7.5 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:gitjournal/core/note.dart';
import 'package:gitjournal/core/notes_folder.dart';
import 'package:gitjournal/utils/link_resolver.dart';
class Node {
Note note;
double y = 0.0;
double x = 0.0;
bool pressed = false;
double forceX = 0.0;
double forceY = 0.0;
String _label;
Node(this.note);
String get label {
_label ??= note.pathSpec();
return _label;
}
@override
String toString() => "Node{$label, $x, $y}";
}
class Edge {
Node a;
Node b;
Edge(this.a, this.b);
}
class Graph extends ChangeNotifier {
List<Node> nodes = [];
List<Edge> edges = [];
Map<String, Set<int>> _neighbours = {};
Map<String, int> _nodeIndexes;
GraphNodeLayout initLayouter;
final double nodeSize = 50.0;
Graph.fromFolder(NotesFolder folder) {
initLayouter = GraphNodeLayout(maxHeight: 2000, maxWidth: 2000);
// print("Building graph .... ");
_addFolder(folder).then((_) {
// print("Done Building graph");
// print("Starting layouting ...");
//startLayout();
});
}
Future<void> _addFolder(NotesFolder folder) async {
for (var note in folder.notes) {
await _addNote(note);
}
for (var subFolder in folder.subFolders) {
await _addFolder(subFolder);
}
notifyListeners();
}
Future<void> _addNote(Note note) async {
var node = _getNode(note);
var links = await node.note.fetchLinks();
var linkResolver = LinkResolver(note);
for (var l in links) {
var noteB = linkResolver.resolveLink(l);
if (noteB == null) {
// print("not found $l");
continue;
}
var edge = Edge(node, _getNode(noteB));
edges.add(edge);
}
}
// FIXME: Make this faster?
Node _getNode(Note note) {
var i = nodes.indexWhere((n) => n.note.filePath == note.filePath);
if (i == -1) {
var node = Node(note);
initLayouter.positionNode(node);
nodes.add(node);
return node;
}
return nodes[i];
}
void notify() {
notifyListeners();
startLayout();
}
List<int> computeNeighbours(Node n) {
if (_nodeIndexes == null) {
_nodeIndexes = <String, int>{};
for (var i = 0; i < this.nodes.length; i++) {
var node = this.nodes[i];
_nodeIndexes[node.label] = i;
}
}
var _nodes = _neighbours[n.label];
if (_nodes != null) {
return _nodes.union(computeOverlappingNodes(n)).toList();
}
var nodes = <int>{};
for (var edge in edges) {
if (edge.a.label == n.label) {
nodes.add(_nodeIndexes[edge.b.label]);
continue;
}
if (edge.b.label == n.label) {
nodes.add(_nodeIndexes[edge.a.label]);
continue;
}
}
_neighbours[n.label] = nodes;
return nodes.union(computeOverlappingNodes(n)).toList();
}
// These nodes aren't actually neighbours, but we don't want nodes to
// ever overlap, so I'm making the ones that are close by neighbours
Set<int> computeOverlappingNodes(Node n) {
var _nodes = <int>{};
for (var i = 0; i < nodes.length; i++) {
var node = nodes[i];
if (node.label == n.label) {
continue;
}
var dx = node.x - n.x;
var dy = node.y - n.y;
var dist = sqrt((dx * dx) + (dy * dy));
if (dist <= 60) {
// print('${node.label} and ${n.label} are too close - $dist');
_nodes.add(i);
}
}
return _nodes;
}
Timer layoutTimer;
void startLayout() {
if (layoutTimer != null) {
return;
}
const interval = Duration(milliseconds: 25);
layoutTimer = Timer.periodic(interval, (Timer t) {
bool shouldStop = _updateGraphPositions(this);
// print("shouldStop $shouldStop");
if (shouldStop) {
stopLayout();
}
});
/*
Timer(5.seconds, () {
if (layoutTimer != null) {
layoutTimer.cancel();
layoutTimer = null;
}
});*/
}
void stopLayout() {
if (layoutTimer != null) {
layoutTimer.cancel();
layoutTimer = null;
}
}
}
class GraphNodeLayout {
final double maxWidth;
final double maxHeight;
double x = 0.0;
double y = 0.0;
double startX = 60.0;
double startY = 60.0;
double gap = 70;
double nodeSize = 50;
GraphNodeLayout({@required this.maxWidth, @required this.maxHeight}) {
x = startX;
y = startY;
}
void positionNode(Node node) {
node.x = x;
node.y = y;
x += gap;
if (x + nodeSize >= maxWidth) {
x = startX;
y += gap;
}
}
}
//
// Basic Force Directed Layout
//
const l = 150.0; // sping rest length
const k_r = 1000.0; // repulsive force constant
const k_s = 20; // spring constant
const delta_t = 0.5; // time step
const MAX_DISPLACEMENT_SQUARED = 16;
const min_movement = 1.0;
/*
Original Values from main_graph.dart
const l = 150.0; // sping rest length
const k_r = 10000.0; // repulsive force constant
const k_s = 20; // spring constant
const delta_t = 0.005; // time step
const MAX_DISPLACEMENT_SQUARED = 16;
const min_movement = 1.0;
*/
bool _updateGraphPositions(Graph g) {
var numNodes = g.nodes.length;
// Initialize net forces
for (var i = 0; i < numNodes; i++) {
g.nodes[i].forceX = 0;
g.nodes[i].forceY = 0;
}
for (var i1 = 0; i1 < numNodes - 1; i1++) {
var node1 = g.nodes[i1];
for (var i2 = i1 + 1; i2 < numNodes; i2++) {
var node2 = g.nodes[i2];
var dx = node2.x - node1.x;
var dy = node2.y - node1.y;
if (dx != 0 || dy != 0) {
var distSq = (dx * dx) + (dy * dy);
var distance = sqrt(distSq);
var force = k_r / distSq;
var fx = force * dx / distance;
var fy = force * dy / distance;
node1.forceX -= fx;
node1.forceY -= fy;
node2.forceX += fx;
node2.forceY += fy;
}
}
}
// Spring forces between adjacent pairs
for (var i1 = 0; i1 < numNodes; i1++) {
var node1 = g.nodes[i1];
var node1Neighbours = g.computeNeighbours(node1);
for (var j = 0; j < node1Neighbours.length; j++) {
var i2 = node1Neighbours[j];
var node2 = g.nodes[i2];
if (i1 < i2) {
var dx = node2.x - node1.x;
var dy = node2.y - node1.y;
if (dx != 0 || dy != 0) {
var distSq = (dx * dx) + (dy * dy);
var distance = sqrt(distSq);
var force = k_s * (distance - l);
var fx = force * dx / distance;
var fy = force * dy / distance;
node1.forceX += fx;
node1.forceY += fy;
node2.forceX -= fx;
node2.forceY -= fy;
}
}
}
}
// Update positions
var allBelowThreshold = true;
for (var i = 0; i < numNodes; i++) {
var node = g.nodes[i];
// Skip Node which is current being controlled
if (node.pressed) {
continue;
}
var dx = delta_t * node.forceX;
var dy = delta_t * node.forceY;
var dispSq = (dx * dx) + (dy * dy);
if (dispSq > MAX_DISPLACEMENT_SQUARED) {
var s = sqrt(MAX_DISPLACEMENT_SQUARED / dispSq);
dx *= s;
dy *= s;
}
// print('${node.label} $dx $dy');
if (node.x - dx <= g.nodeSize / 2) {
node.x = (g.nodeSize / 2) + 1;
continue;
}
if (node.y - dy <= g.nodeSize / 2) {
node.y = (g.nodeSize / 2) + 1;
continue;
}
node.x += dx;
node.y += dy;
if (dx.abs() > min_movement || dy.abs() > min_movement) {
allBelowThreshold = false;
}
}
// print('------------------');
g.notify();
return allBelowThreshold;
}