mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-08-06 15:21:21 +08:00

I'm testing this with the Foam documentation. This has revealed a number of bugs in our link resolver - most of which have now been fixed. The graph layouting is not being done as this is so incredibly slow. This has been added as an experimental feature.
314 lines
6.7 KiB
Dart
314 lines
6.7 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;
|
|
|
|
Node(this.note);
|
|
|
|
String get label => note.pathSpec();
|
|
|
|
@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;
|
|
|
|
var x = 1050.0;
|
|
var y = 1050.0;
|
|
|
|
Graph.fromFolder(NotesFolder folder) {
|
|
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;
|
|
}
|
|
|
|
print("Adding edge ..");
|
|
|
|
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);
|
|
node.x = x;
|
|
node.y = y;
|
|
|
|
if (x >= 500) {
|
|
x = 50;
|
|
y += 50;
|
|
}
|
|
x += 50;
|
|
|
|
//print('${node.label} -> ${node.x} ${node.y}');
|
|
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;
|
|
}
|
|
|
|
void assignRandomPositions(int maxX, int maxY) {
|
|
var random = Random(DateTime.now().millisecondsSinceEpoch);
|
|
|
|
for (var node in nodes) {
|
|
node.x = random.nextInt(maxX).toDouble();
|
|
node.y = random.nextInt(maxY).toDouble();
|
|
}
|
|
|
|
notifyListeners();
|
|
}
|
|
|
|
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) {
|
|
layoutTimer.cancel();
|
|
layoutTimer = null;
|
|
}
|
|
});
|
|
|
|
/*
|
|
Timer(const Duration(seconds: 5), () {
|
|
if (layoutTimer != null) {
|
|
layoutTimer.cancel();
|
|
layoutTimer = null;
|
|
}
|
|
});*/
|
|
}
|
|
}
|
|
|
|
//
|
|
// Basic Force Directed Layout
|
|
//
|
|
const l = 150.0; // sping rest length
|
|
const k_r = 10000.0; // repulsive force constant
|
|
const k_s = 20; // spring constant
|
|
const delta_t = 1.0; // 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');
|
|
node.x += dx;
|
|
node.y += dy;
|
|
|
|
if (dx.abs() > min_movement || dy.abs() > min_movement) {
|
|
allBelowThreshold = false;
|
|
}
|
|
}
|
|
print('------------------');
|
|
|
|
g.notify();
|
|
return allBelowThreshold;
|
|
}
|