diff --git a/src/public/app/services/link_map.js b/src/public/app/services/link_map.js
index bb4241181..14f266323 100644
--- a/src/public/app/services/link_map.js
+++ b/src/public/app/services/link_map.js
@@ -3,203 +3,5 @@ import server from "./server.js";
import froca from "./froca.js";
export default class LinkMap {
- constructor(note, $linkMapContainer, options = {}) {
- this.note = note;
- this.options = Object.assign({
- maxDepth: 10,
- maxNotes: 100,
- zoom: 1.0
- }, options);
- this.$linkMapContainer = $linkMapContainer;
-
- this.zoomLevel = 1;
- }
-
- setZoomLevel(level) {
- this.zoomLevel = level;
- }
-
- async render() {
- await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH);
-
- this.graph = ForceGraph()(this.$linkMapContainer[0])
- .width(this.options.width)
- .height(this.options.height)
- .onZoom(zoom => this.setZoomLevel(zoom.k))
- .nodeRelSize(7)
- .nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
- .nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
- .nodeLabel(node => node.name)
- .maxZoom(5)
- .nodePointerAreaPaint((node, color, ctx) => {
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
- ctx.fill();
- })
- .linkLabel(l => `${l.source.name} - ${l.name} - ${l.target.name}`)
- .linkCanvasObject((link, ctx) => this.paintLink(link, ctx))
- .linkCanvasObjectMode(() => "after")
- .linkDirectionalArrowLength(4)
- .linkDirectionalArrowRelPos(1)
- .linkWidth(2)
- .linkColor("#ddd")
- .d3VelocityDecay(0.2)
- .onNodeClick(node => this.nodeClicked(node));
-
- this.graph.d3Force('link').distance(50);
-
- this.graph.d3Force('center').strength(0.9);
-
- this.graph.d3Force('charge').strength(-30);
- this.graph.d3Force('charge').distanceMax(400);
-
- this.renderData(await this.loadNotesAndRelations());
- }
-
- renderData(data, zoomToFit = true, zoomPadding = 10) {
- this.graph.graphData(data);
-
- if (zoomToFit) {
- setTimeout(() => this.graph.zoomToFit(400, zoomPadding), 1000);
- }
- }
-
- setHeight(height) {
- this.graph.height(height);
- }
-
- centerOnNode(node) {
- this.nodeClicked(node);
-
- this.graph.centerAt(node.x, node.y, 1000);
- this.graph.zoom(6, 2000);
- }
-
- async nodeClicked(node) {
- if (!node.expanded) {
- const neighborGraph = await fetchNeighborGraph(node.id);
-
- addToTasGraph(neighborGraph);
-
- renderData(getTasGraph(), false);
- }
- }
-
- async loadNotesAndRelations(options = {}) {
- this.options = Object.assign(this.options, options);
-
- const links = await server.post(`notes/${this.note.noteId}/link-map`, {
- maxNotes: this.options.maxNotes,
- maxDepth: this.options.maxDepth
- });
-
- const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
-
- if (noteIds.size === 0) {
- noteIds.add(this.note.noteId);
- }
-
- // preload all notes
- const notes = await froca.getNotes(Array.from(noteIds), true);
-
- return {
- nodes: notes.map(note => ({
- id: note.noteId,
- name: note.title,
- type: note.type
- })),
- links: links.map(link => ({
- id: link.noteId + "-" + link.name + "-" + link.targetNoteId,
- source: link.noteId,
- target: link.targetNoteId,
- name: link.name
- }))
- };
- }
-
- cleanup() {
- }
-
- paintLink(link, ctx) {
- if (this.zoomLevel < 3) {
- return;
- }
-
- ctx.font = '3px MontserratLight';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillStyle = "grey";
-
- const {source, target} = link;
-
- const x = (source.x + target.x) / 2;
- const y = (source.y + target.y) / 2;
-
- ctx.save();
- ctx.translate(x, y);
-
- const deltaY = source.y - target.y;
- const deltaX = source.x - target.x;
-
- let angle = Math.atan2(deltaY, deltaX);
- let moveY = 2;
-
- if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
- angle += Math.PI;
- moveY = -2;
- }
-
- ctx.rotate(angle);
- ctx.fillText(link.name, 0, moveY);
- ctx.restore();
- }
-
- paintNode(node, color, ctx) {
- const {x, y} = node;
-
- ctx.fillStyle = color;
- ctx.beginPath();
- ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
- ctx.fill();
-
- if (this.zoomLevel < 2) {
- return;
- }
-
- if (!node.expanded) {
- ctx.fillStyle = color;
- ctx.font = 10 + 'px MontserratLight';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
- ctx.fillText("+", x, y + 1);
- }
-
- ctx.fillStyle = "#555";
- ctx.font = 5 + 'px MontserratLight';
- ctx.textAlign = 'center';
- ctx.textBaseline = 'middle';
-
- let title = node.name;
-
- if (title.length > 15) {
- title = title.substr(0, 15) + "...";
- }
-
- ctx.fillText(title, x, y + 7);
- }
-
- stringToColor(str) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = str.charCodeAt(i) + ((hash << 5) - hash);
- }
- let colour = '#';
- for (let i = 0; i < 3; i++) {
- const value = (hash >> (i * 8)) & 0xFF;
- colour += ('00' + value.toString(16)).substr(-2);
- }
- return colour;
- }
}
diff --git a/src/public/app/widgets/section_widgets/link_map.js b/src/public/app/widgets/section_widgets/link_map.js
index 593d14e69..a06314158 100644
--- a/src/public/app/widgets/section_widgets/link_map.js
+++ b/src/public/app/widgets/section_widgets/link_map.js
@@ -1,5 +1,7 @@
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import froca from "../../services/froca.js";
+import libraryLoader from "../../services/library_loader.js";
+import server from "../../services/server.js";
const TPL = `
@@ -42,6 +44,7 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
+ this.$container = this.$widget.find(".link-map-container");
this.$openFullButton = this.$widget.find('.open-full-button');
this.$openFullButton.on('click', () => {
@@ -51,7 +54,7 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
this.$widget.find('.link-map-container').css("height", maxHeight);
- this.linkMapService.setHeight(maxHeight);
+ this.graph.height(maxHeight);
this.$openFullButton.hide();
this.$collapseButton.show();
@@ -61,39 +64,195 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
this.$collapseButton.on('click', () => {
this.$widget.find('.link-map-container,.force-graph-container,canvas').css("height", 300);
- this.linkMapService.setHeight(300);
+ this.graph.height(300);
this.$openFullButton.show();
this.$collapseButton.hide();
});
-
this.overflowing();
}
- async refreshWithNote(note) {
- this.$widget.find(".link-map-container").empty();
-
- const $linkMapContainer = this.$widget.find('.link-map-container');
-
- const LinkMapServiceClass = (await import('../../services/link_map.js')).default;
-
- this.linkMapService = new LinkMapServiceClass(note, $linkMapContainer, {
- maxDepth: 3,
- zoom: 0.6,
- width: $linkMapContainer.width(),
- height: $linkMapContainer.height()
- });
-
- await this.linkMapService.render();
+ setZoomLevel(level) {
+ this.zoomLevel = level;
}
- cleanup() {
- if (this.linkMapService) {
- this.linkMapService.cleanup();
+ async refreshWithNote(note) {
+ this.$container.empty();
+
+ await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH);
+
+ this.graph = ForceGraph()(this.$container[0])
+ .width(this.$container.width())
+ .height(this.$container.height())
+ .onZoom(zoom => this.setZoomLevel(zoom.k))
+ .nodeRelSize(7)
+ .nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
+ .nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
+ .nodeLabel(node => node.name)
+ .maxZoom(5)
+ .nodePointerAreaPaint((node, color, ctx) => {
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false);
+ ctx.fill();
+ })
+ .linkLabel(l => `${l.source.name} - ${l.name} - ${l.target.name}`)
+ .linkCanvasObject((link, ctx) => this.paintLink(link, ctx))
+ .linkCanvasObjectMode(() => "after")
+ .linkDirectionalArrowLength(4)
+ .linkDirectionalArrowRelPos(1)
+ .linkWidth(2)
+ .linkColor("#ddd")
+ .d3VelocityDecay(0.2)
+ .onNodeClick(node => this.nodeClicked(node));
+
+ this.graph.d3Force('link').distance(50);
+
+ this.graph.d3Force('center').strength(0.9);
+
+ this.graph.d3Force('charge').strength(-30);
+ this.graph.d3Force('charge').distanceMax(400);
+
+ this.renderData(await this.loadNotesAndRelations());
+ }
+
+ renderData(data, zoomToFit = true, zoomPadding = 10) {
+ this.graph.graphData(data);
+
+ if (zoomToFit) {
+ setTimeout(() => this.graph.zoomToFit(400, zoomPadding), 1000);
}
}
+ centerOnNode(node) {
+ this.nodeClicked(node);
+
+ this.graph.centerAt(node.x, node.y, 1000);
+ this.graph.zoom(6, 2000);
+ }
+
+ async nodeClicked(node) {
+ if (!node.expanded) {
+ const neighborGraph = await fetchNeighborGraph(node.id);
+
+ addToTasGraph(neighborGraph);
+
+ renderData(getTasGraph(), false);
+ }
+ }
+
+ async loadNotesAndRelations(options = {}) {
+ const links = await server.post(`notes/${this.note.noteId}/link-map`, {
+ maxNotes: 30,
+ maxDepth: 5
+ });
+
+ const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
+
+ if (noteIds.size === 0) {
+ noteIds.add(this.note.noteId);
+ }
+
+ // preload all notes
+ const notes = await froca.getNotes(Array.from(noteIds), true);
+
+ return {
+ nodes: notes.map(note => ({
+ id: note.noteId,
+ name: note.title,
+ type: note.type
+ })),
+ links: links.map(link => ({
+ id: link.noteId + "-" + link.name + "-" + link.targetNoteId,
+ source: link.noteId,
+ target: link.targetNoteId,
+ name: link.name
+ }))
+ };
+ }
+
+ paintLink(link, ctx) {
+ if (this.zoomLevel < 3) {
+ return;
+ }
+
+ ctx.font = '3px MontserratLight';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillStyle = "grey";
+
+ const {source, target} = link;
+
+ const x = (source.x + target.x) / 2;
+ const y = (source.y + target.y) / 2;
+
+ ctx.save();
+ ctx.translate(x, y);
+
+ const deltaY = source.y - target.y;
+ const deltaX = source.x - target.x;
+
+ let angle = Math.atan2(deltaY, deltaX);
+ let moveY = 2;
+
+ if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
+ angle += Math.PI;
+ moveY = -2;
+ }
+
+ ctx.rotate(angle);
+ ctx.fillText(link.name, 0, moveY);
+ ctx.restore();
+ }
+
+ paintNode(node, color, ctx) {
+ const {x, y} = node;
+
+ ctx.fillStyle = color;
+ ctx.beginPath();
+ ctx.arc(x, y, 4, 0, 2 * Math.PI, false);
+ ctx.fill();
+
+ if (this.zoomLevel < 2) {
+ return;
+ }
+
+ if (!node.expanded) {
+ ctx.fillStyle = color;
+ ctx.font = 10 + 'px MontserratLight';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.fillText("+", x, y + 1);
+ }
+
+ ctx.fillStyle = "#555";
+ ctx.font = 5 + 'px MontserratLight';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+
+ let title = node.name;
+
+ if (title.length > 15) {
+ title = title.substr(0, 15) + "...";
+ }
+
+ ctx.fillText(title, x, y + 7);
+ }
+
+ stringToColor(str) {
+ let hash = 0;
+ for (let i = 0; i < str.length; i++) {
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
+ }
+ let colour = '#';
+ for (let i = 0; i < 3; i++) {
+ const value = (hash >> (i * 8)) & 0xFF;
+ colour += ('00' + value.toString(16)).substr(-2);
+ }
+ return colour;
+ }
+
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes().find(attr => attr.type === 'relation' && (attr.noteId === this.noteId || attr.value === this.noteId))) {
this.noteSwitched();