2019-08-28 02:20:00 +08:00
|
|
|
import libraryLoader from "./library_loader.js";
|
|
|
|
import server from "./server.js";
|
|
|
|
import treeCache from "./tree_cache.js";
|
|
|
|
import linkService from "./link.js";
|
|
|
|
|
|
|
|
const linkOverlays = [
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 1,
|
|
|
|
id: "arrow",
|
|
|
|
length: 10,
|
|
|
|
width: 10,
|
|
|
|
foldback: 0.7
|
|
|
|
} ]
|
|
|
|
];
|
|
|
|
|
|
|
|
export default class LinkMap {
|
2019-08-28 04:19:32 +08:00
|
|
|
constructor(note, $linkMapContainer, options = {}) {
|
2019-08-28 02:20:00 +08:00
|
|
|
this.note = note;
|
2019-08-29 02:29:10 +08:00
|
|
|
this.options = Object.assign({
|
2019-08-28 04:19:32 +08:00
|
|
|
maxDepth: 10,
|
2019-08-28 04:47:10 +08:00
|
|
|
maxNotes: 30,
|
2019-08-28 04:19:32 +08:00
|
|
|
zoom: 1.0
|
|
|
|
}, options);
|
|
|
|
|
2019-08-28 02:20:00 +08:00
|
|
|
this.$linkMapContainer = $linkMapContainer;
|
|
|
|
this.linkMapContainerId = this.$linkMapContainer.attr("id");
|
|
|
|
}
|
|
|
|
|
|
|
|
async render() {
|
|
|
|
await libraryLoader.requireLibrary(libraryLoader.LINK_MAP);
|
|
|
|
|
|
|
|
jsPlumb.ready(() => {
|
|
|
|
this.initJsPlumbInstance();
|
|
|
|
|
|
|
|
this.initPanZoom();
|
|
|
|
|
|
|
|
this.loadNotesAndRelations();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-08-28 04:47:10 +08:00
|
|
|
async loadNotesAndRelations(options = {}) {
|
2019-08-29 02:29:10 +08:00
|
|
|
this.options = Object.assign(this.options, options);
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-08-28 04:47:10 +08:00
|
|
|
this.cleanup();
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-08-28 04:19:32 +08:00
|
|
|
const links = await server.post(`notes/${this.note.noteId}/link-map`, {
|
2019-08-28 04:47:10 +08:00
|
|
|
maxNotes: this.options.maxNotes,
|
2019-08-28 04:19:32 +08:00
|
|
|
maxDepth: this.options.maxDepth
|
2019-08-28 02:20:00 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
|
|
|
|
|
|
|
|
if (noteIds.size === 0) {
|
2019-08-28 04:19:32 +08:00
|
|
|
noteIds.add(this.note.noteId);
|
2019-08-28 02:20:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
// preload all notes
|
2019-09-08 17:25:57 +08:00
|
|
|
const notes = await treeCache.getNotes(Array.from(noteIds), true);
|
2019-08-28 02:20:00 +08:00
|
|
|
|
|
|
|
const graph = new Springy.Graph();
|
|
|
|
graph.addNodes(...noteIds);
|
2019-08-28 04:19:32 +08:00
|
|
|
graph.addEdges(...links.map(l => [l.noteId, l.targetNoteId]));
|
2019-08-28 02:20:00 +08:00
|
|
|
|
|
|
|
const layout = new Springy.Layout.ForceDirected(
|
|
|
|
graph,
|
2019-08-29 03:15:16 +08:00
|
|
|
// param explanation here: https://github.com/dhotson/springy/issues/58
|
2019-08-29 04:16:12 +08:00
|
|
|
400.0, // Spring stiffness
|
2019-08-31 04:01:49 +08:00
|
|
|
600.0, // Node repulsion
|
|
|
|
0.15, // Damping
|
|
|
|
0.1 // min energy threshold
|
2019-08-28 02:20:00 +08:00
|
|
|
);
|
|
|
|
|
|
|
|
const getNoteBox = noteId => {
|
|
|
|
const noteBoxId = this.noteIdToId(noteId);
|
|
|
|
const $existingNoteBox = $("#" + noteBoxId);
|
|
|
|
|
|
|
|
if ($existingNoteBox.length > 0) {
|
|
|
|
return $existingNoteBox;
|
|
|
|
}
|
|
|
|
|
|
|
|
const note = notes.find(n => n.noteId === noteId);
|
|
|
|
|
2019-08-30 05:08:30 +08:00
|
|
|
if (!note) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2019-08-28 02:20:00 +08:00
|
|
|
const $noteBox = $("<div>")
|
|
|
|
.addClass("note-box")
|
|
|
|
.prop("id", noteBoxId);
|
|
|
|
|
|
|
|
linkService.createNoteLink(noteId, note.title).then($link => {
|
2019-11-10 00:39:48 +08:00
|
|
|
$link.on('click', e => {
|
2019-09-10 02:29:07 +08:00
|
|
|
try {
|
|
|
|
$link.tooltip('dispose');
|
|
|
|
}
|
|
|
|
catch (e) {}
|
|
|
|
|
|
|
|
linkService.goToLink(e);
|
|
|
|
});
|
|
|
|
|
2019-08-28 02:20:00 +08:00
|
|
|
$noteBox.append($("<span>").addClass("title").append($link));
|
|
|
|
});
|
|
|
|
|
2019-08-28 04:19:32 +08:00
|
|
|
if (noteId === this.note.noteId) {
|
2019-08-28 02:20:00 +08:00
|
|
|
$noteBox.addClass("link-map-active-note");
|
|
|
|
}
|
|
|
|
|
2019-08-29 04:16:12 +08:00
|
|
|
$noteBox
|
2019-09-05 03:30:11 +08:00
|
|
|
.mouseover(() => this.$linkMapContainer.find(".link-" + noteId).addClass("jsplumb-connection-hover"))
|
|
|
|
.mouseout(() => this.$linkMapContainer.find(".link-" + noteId).removeClass("jsplumb-connection-hover"));
|
2019-08-29 04:16:12 +08:00
|
|
|
|
2019-08-28 02:20:00 +08:00
|
|
|
this.$linkMapContainer.append($noteBox);
|
|
|
|
|
|
|
|
this.jsPlumbInstance.draggable($noteBox[0], {
|
|
|
|
start: params => {
|
2019-08-28 04:19:32 +08:00
|
|
|
this.renderer.stop();
|
2019-08-28 02:20:00 +08:00
|
|
|
},
|
|
|
|
drag: params => {},
|
|
|
|
stop: params => {}
|
|
|
|
});
|
|
|
|
|
|
|
|
return $noteBox;
|
|
|
|
};
|
|
|
|
|
2019-11-06 03:59:20 +08:00
|
|
|
this.renderer = new Springy.Renderer(layout);
|
|
|
|
await this.renderer.start(500);
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-11-06 03:59:20 +08:00
|
|
|
layout.eachNode((node, point) => {
|
|
|
|
const $noteBox = getNoteBox(node.id);
|
|
|
|
const middleW = this.$linkMapContainer.width() / 2;
|
|
|
|
const middleH = this.$linkMapContainer.height() / 2;
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-11-06 03:59:20 +08:00
|
|
|
$noteBox
|
|
|
|
.css("left", (middleW + point.p.x * 100) + "px")
|
|
|
|
.css("top", (middleH + point.p.y * 100) + "px");
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-11-06 03:59:20 +08:00
|
|
|
if ($noteBox.hasClass("link-map-active-note")) {
|
|
|
|
this.moveToCenterOfElement($noteBox[0]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
layout.eachEdge(edge => {
|
|
|
|
const connectionId = this.linkMapContainerId + '-' + edge.source.id + '-' + edge.target.id;
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-11-06 03:59:20 +08:00
|
|
|
if ($("#" + connectionId).length > 0) {
|
|
|
|
return;
|
2019-08-28 02:20:00 +08:00
|
|
|
}
|
|
|
|
|
2019-11-06 03:59:20 +08:00
|
|
|
getNoteBox(edge.source.id);
|
|
|
|
getNoteBox(edge.target.id);
|
|
|
|
|
|
|
|
const connection = this.jsPlumbInstance.connect({
|
|
|
|
source: this.noteIdToId(edge.source.id),
|
|
|
|
target: this.noteIdToId(edge.target.id),
|
|
|
|
type: 'link'
|
|
|
|
});
|
|
|
|
|
|
|
|
if (connection) {
|
|
|
|
$(connection.canvas)
|
|
|
|
.prop("id", connectionId)
|
|
|
|
.addClass('link-' + edge.source.id)
|
|
|
|
.addClass('link-' + edge.target.id);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
console.log(`connection not created for`, edge);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
this.jsPlumbInstance.repaintEverything();
|
2019-08-28 02:20:00 +08:00
|
|
|
}
|
|
|
|
|
2019-08-29 05:08:05 +08:00
|
|
|
moveToCenterOfElement(element) {
|
|
|
|
const elemBounds = element.getBoundingClientRect();
|
|
|
|
const containerBounds = this.pzInstance.getOwner().getBoundingClientRect();
|
|
|
|
|
|
|
|
const centerX = -elemBounds.left + containerBounds.left + (containerBounds.width / 2) - (elemBounds.width / 2);
|
|
|
|
const centerY = -elemBounds.top + containerBounds.top + (containerBounds.height / 2) - (elemBounds.height / 2);
|
|
|
|
|
|
|
|
const transform = this.pzInstance.getTransform();
|
|
|
|
|
|
|
|
const newX = transform.x + centerX;
|
|
|
|
const newY = transform.y + centerY;
|
|
|
|
|
|
|
|
this.pzInstance.moveTo(newX, newY);
|
|
|
|
}
|
|
|
|
|
2019-08-28 02:20:00 +08:00
|
|
|
initPanZoom() {
|
|
|
|
if (this.pzInstance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.pzInstance = panzoom(this.$linkMapContainer[0], {
|
|
|
|
maxZoom: 2,
|
|
|
|
minZoom: 0.3,
|
|
|
|
smoothScroll: false,
|
|
|
|
filterKey: function (e, dx, dy, dz) {
|
|
|
|
// if ALT is pressed then panzoom should bubble the event up
|
|
|
|
// this is to preserve ALT-LEFT, ALT-RIGHT navigation working
|
|
|
|
return e.altKey;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanup() {
|
|
|
|
if (this.renderer) {
|
|
|
|
this.renderer.stop();
|
|
|
|
}
|
|
|
|
|
2019-09-07 04:18:03 +08:00
|
|
|
if (this.jsPlumbInstance) {
|
|
|
|
// delete all endpoints and connections
|
|
|
|
// this is done at this point (after async operations) to reduce flicker to the minimum
|
|
|
|
this.jsPlumbInstance.deleteEveryEndpoint();
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-09-07 04:18:03 +08:00
|
|
|
// without this we still end up with note boxes remaining in the canvas
|
|
|
|
this.$linkMapContainer.empty();
|
2019-08-28 02:20:00 +08:00
|
|
|
|
2019-09-07 04:18:03 +08:00
|
|
|
// reset zoom/pan
|
|
|
|
this.pzInstance.zoomAbs(0, 0, this.options.zoom);
|
|
|
|
this.pzInstance.moveTo(0, 0);
|
|
|
|
}
|
2019-08-28 02:20:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
initJsPlumbInstance() {
|
|
|
|
if (this.jsPlumbInstance) {
|
|
|
|
this.cleanup();
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.jsPlumbInstance = jsPlumb.getInstance({
|
|
|
|
Endpoint: ["Blank", {}],
|
|
|
|
ConnectionOverlays: linkOverlays,
|
|
|
|
PaintStyle: { stroke: "var(--muted-text-color)", strokeWidth: 1 },
|
|
|
|
HoverPaintStyle: { stroke: "var(--main-text-color)", strokeWidth: 1 },
|
|
|
|
Container: this.$linkMapContainer.attr("id")
|
|
|
|
});
|
|
|
|
|
|
|
|
this.jsPlumbInstance.registerConnectionType("link", { anchor: "Continuous", connector: "Straight", overlays: linkOverlays });
|
|
|
|
}
|
|
|
|
|
|
|
|
noteIdToId(noteId) {
|
|
|
|
return this.linkMapContainerId + "-note-" + noteId;
|
|
|
|
}
|
|
|
|
}
|