2019-06-02 23:12:18 +08:00
|
|
|
import server from '../services/server.js';
|
|
|
|
import noteDetailService from "../services/note_detail.js";
|
2019-06-04 04:55:59 +08:00
|
|
|
import libraryLoader from "../services/library_loader.js";
|
2019-06-05 04:57:10 +08:00
|
|
|
import treeCache from "../services/tree_cache.js";
|
2019-06-04 04:55:59 +08:00
|
|
|
|
|
|
|
const $linkMapContainer = $("#link-map-container");
|
|
|
|
|
|
|
|
const uniDirectionalOverlays = [
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 1,
|
|
|
|
id: "arrow",
|
|
|
|
length: 14,
|
|
|
|
foldback: 0.8
|
|
|
|
} ],
|
|
|
|
[ "Label", { label: "", id: "label", cssClass: "connection-label" }]
|
|
|
|
];
|
|
|
|
|
|
|
|
const biDirectionalOverlays = [
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 1,
|
|
|
|
id: "arrow",
|
|
|
|
length: 14,
|
|
|
|
foldback: 0.8
|
|
|
|
} ],
|
|
|
|
[ "Label", { label: "", id: "label", cssClass: "connection-label" }],
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 0,
|
|
|
|
id: "arrow2",
|
|
|
|
length: 14,
|
|
|
|
direction: -1,
|
|
|
|
foldback: 0.8
|
|
|
|
} ]
|
|
|
|
];
|
|
|
|
|
|
|
|
const inverseRelationsOverlays = [
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 1,
|
|
|
|
id: "arrow",
|
|
|
|
length: 14,
|
|
|
|
foldback: 0.8
|
|
|
|
} ],
|
|
|
|
[ "Label", { label: "", location: 0.2, id: "label-source", cssClass: "connection-label" }],
|
|
|
|
[ "Label", { label: "", location: 0.8, id: "label-target", cssClass: "connection-label" }],
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 0,
|
|
|
|
id: "arrow2",
|
|
|
|
length: 14,
|
|
|
|
direction: -1,
|
|
|
|
foldback: 0.8
|
|
|
|
} ]
|
|
|
|
];
|
|
|
|
|
|
|
|
const linkOverlays = [
|
|
|
|
[ "Arrow", {
|
|
|
|
location: 1,
|
|
|
|
id: "arrow",
|
|
|
|
length: 14,
|
|
|
|
foldback: 0.8
|
|
|
|
} ]
|
|
|
|
];
|
2019-06-02 23:12:18 +08:00
|
|
|
|
2019-06-02 21:35:57 +08:00
|
|
|
const $dialog = $("#link-map-dialog");
|
|
|
|
|
2019-06-04 04:55:59 +08:00
|
|
|
let jsPlumbInstance = null;
|
2019-06-09 17:50:11 +08:00
|
|
|
let pzInstance = null;
|
2019-06-04 04:55:59 +08:00
|
|
|
|
2019-06-02 21:35:57 +08:00
|
|
|
async function showDialog() {
|
|
|
|
glob.activeDialog = $dialog;
|
|
|
|
|
2019-06-09 17:50:11 +08:00
|
|
|
await libraryLoader.requireLibrary(libraryLoader.LINK_MAP);
|
2019-06-02 23:12:18 +08:00
|
|
|
|
2019-06-04 04:55:59 +08:00
|
|
|
jsPlumb.ready(() => {
|
|
|
|
initJsPlumbInstance();
|
2019-06-02 23:12:18 +08:00
|
|
|
|
2019-06-09 17:50:11 +08:00
|
|
|
initPanZoom();
|
|
|
|
|
2019-06-04 04:55:59 +08:00
|
|
|
loadNotesAndRelations();
|
|
|
|
});
|
2019-06-02 23:12:18 +08:00
|
|
|
|
2019-06-02 21:35:57 +08:00
|
|
|
$dialog.modal();
|
|
|
|
}
|
|
|
|
|
2019-06-04 04:55:59 +08:00
|
|
|
async function loadNotesAndRelations() {
|
|
|
|
const noteId = noteDetailService.getActiveNoteId();
|
|
|
|
|
|
|
|
const links = await server.get(`notes/${noteId}/link-map`);
|
|
|
|
|
|
|
|
const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
|
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
// preload all notes
|
|
|
|
const notes = await treeCache.getNotes(Array.from(noteIds));
|
|
|
|
|
2019-06-04 04:55:59 +08:00
|
|
|
const graph = new Springy.Graph();
|
|
|
|
graph.addNodes(...noteIds);
|
2019-06-05 04:57:10 +08:00
|
|
|
graph.addEdges(...links.map(l => [l.noteId, l.targetNoteId]));
|
2019-06-04 04:55:59 +08:00
|
|
|
|
|
|
|
const layout = new Springy.Layout.ForceDirected(
|
|
|
|
graph,
|
|
|
|
400.0, // Spring stiffness
|
|
|
|
400.0, // Node repulsion
|
|
|
|
0.5 // Damping
|
|
|
|
);
|
|
|
|
|
2019-06-10 01:18:14 +08:00
|
|
|
function getNoteBox(noteId) {
|
|
|
|
const noteBoxId = noteIdToId(noteId);
|
|
|
|
const $existingNoteBox = $("#" + noteBoxId);
|
|
|
|
|
|
|
|
if ($existingNoteBox.length > 0) {
|
|
|
|
return $existingNoteBox;
|
|
|
|
}
|
|
|
|
|
|
|
|
const note = notes.find(n => n.noteId === noteId);
|
|
|
|
|
|
|
|
const $noteBox = $("<div>")
|
|
|
|
.addClass("note-box")
|
|
|
|
.prop("id", noteBoxId)
|
|
|
|
.append($("<span>").addClass("title").append(note.title));
|
|
|
|
|
|
|
|
jsPlumbInstance.getContainer().appendChild($noteBox[0]);
|
|
|
|
|
|
|
|
return $noteBox;
|
|
|
|
}
|
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
const renderer = new Springy.Renderer(
|
|
|
|
layout,
|
2019-06-10 01:18:14 +08:00
|
|
|
() => {}, //cleanup(),
|
|
|
|
(edge, p1, p2) => {
|
|
|
|
const connectionId = edge.source.id + '-' + edge.target.id;
|
2019-06-05 04:57:10 +08:00
|
|
|
|
2019-06-10 01:18:14 +08:00
|
|
|
if ($("#" + connectionId).length > 0) {
|
|
|
|
return;
|
|
|
|
}
|
2019-06-05 04:57:10 +08:00
|
|
|
|
2019-06-10 01:18:14 +08:00
|
|
|
getNoteBox(edge.source.id);
|
|
|
|
getNoteBox(edge.target.id);
|
2019-06-05 04:57:10 +08:00
|
|
|
|
2019-06-10 01:18:14 +08:00
|
|
|
const connection = jsPlumbInstance.connect({
|
|
|
|
source: noteIdToId(edge.source.id),
|
|
|
|
target: noteIdToId(edge.target.id),
|
|
|
|
type: 'relation' // FIXME
|
2019-06-05 04:57:10 +08:00
|
|
|
});
|
|
|
|
|
2019-06-10 01:18:14 +08:00
|
|
|
connection.canvas.id = connectionId;
|
|
|
|
},
|
|
|
|
(node, p) => {
|
|
|
|
const $noteBox = getNoteBox(node.id);
|
|
|
|
|
|
|
|
$noteBox
|
|
|
|
.css("left", (300 + p.x * 100) + "px")
|
|
|
|
.css("top", (300 + p.y * 100) + "px");
|
|
|
|
},
|
|
|
|
() => {},
|
|
|
|
() => {},
|
|
|
|
() => {
|
|
|
|
jsPlumbInstance.repaintEverything();
|
2019-06-05 04:57:10 +08:00
|
|
|
}
|
|
|
|
);
|
2019-06-04 04:55:59 +08:00
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
renderer.start();
|
2019-06-04 04:55:59 +08:00
|
|
|
}
|
|
|
|
|
2019-06-09 17:50:11 +08:00
|
|
|
function initPanZoom() {
|
|
|
|
if (pzInstance) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
pzInstance = panzoom($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;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-06-10 01:18:14 +08:00
|
|
|
function cleanup() {
|
|
|
|
// delete all endpoints and connections
|
|
|
|
// this is done at this point (after async operations) to reduce flicker to the minimum
|
|
|
|
jsPlumbInstance.deleteEveryEndpoint();
|
|
|
|
|
|
|
|
// without this we still end up with note boxes remaining in the canvas
|
|
|
|
$linkMapContainer.empty();
|
|
|
|
}
|
|
|
|
|
2019-06-04 04:55:59 +08:00
|
|
|
function initJsPlumbInstance() {
|
|
|
|
if (jsPlumbInstance) {
|
2019-06-10 01:18:14 +08:00
|
|
|
cleanup();
|
2019-06-04 04:55:59 +08:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
jsPlumbInstance = jsPlumb.getInstance({
|
2019-06-04 04:55:59 +08:00
|
|
|
Endpoint: ["Dot", {radius: 2}],
|
|
|
|
Connector: "StateMachine",
|
|
|
|
ConnectionOverlays: uniDirectionalOverlays,
|
|
|
|
HoverPaintStyle: { stroke: "#777", strokeWidth: 1 },
|
2019-06-05 04:57:10 +08:00
|
|
|
Container: $linkMapContainer.attr("id")
|
2019-06-04 04:55:59 +08:00
|
|
|
});
|
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
jsPlumbInstance.registerConnectionType("uniDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: uniDirectionalOverlays });
|
|
|
|
|
|
|
|
jsPlumbInstance.registerConnectionType("biDirectional", { anchor:"Continuous", connector:"StateMachine", overlays: biDirectionalOverlays });
|
2019-06-04 04:55:59 +08:00
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
jsPlumbInstance.registerConnectionType("inverse", { anchor:"Continuous", connector:"StateMachine", overlays: inverseRelationsOverlays });
|
2019-06-04 04:55:59 +08:00
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
jsPlumbInstance.registerConnectionType("link", { anchor:"Continuous", connector:"StateMachine", overlays: linkOverlays });
|
|
|
|
}
|
2019-06-04 04:55:59 +08:00
|
|
|
|
2019-06-05 04:57:10 +08:00
|
|
|
function noteIdToId(noteId) {
|
|
|
|
return "link-map-note-" + noteId;
|
2019-06-04 04:55:59 +08:00
|
|
|
}
|
|
|
|
|
2019-06-02 21:35:57 +08:00
|
|
|
export default {
|
|
|
|
showDialog
|
|
|
|
};
|