327 lines
9.3 KiB
JavaScript
Raw Normal View History

2021-05-28 23:19:11 +02:00
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import froca from "../../services/froca.js";
2021-05-31 21:31:07 +02:00
import libraryLoader from "../../services/library_loader.js";
import server from "../../services/server.js";
2021-05-28 23:19:11 +02:00
const TPL = `
<div class="link-map-widget">
2021-05-28 23:52:42 +02:00
<style>
.link-map-widget {
position: relative;
}
.link-map-container {
2021-05-30 23:21:34 +02:00
height: 300px;
}
2021-05-28 23:52:42 +02:00
2021-05-31 21:20:30 +02:00
.open-full-button, .collapse-button {
2021-05-28 23:52:42 +02:00
position: absolute;
2021-05-31 21:20:30 +02:00
right: 5px;
bottom: 5px;
font-size: 180%;
2021-05-28 23:52:42 +02:00
z-index: 1000;
}
</style>
2021-05-31 21:20:30 +02:00
<button class="bx bx-arrow-to-bottom icon-action open-full-button" title="Open full"></button>
<button class="bx bx-arrow-to-top icon-action collapse-button" style="display: none;" title="Collapse to normal size"></button>
2021-05-28 23:52:42 +02:00
2021-05-30 23:21:34 +02:00
<div class="link-map-container"></div>
2021-05-28 23:19:11 +02:00
</div>`;
export default class LinkMapWidget extends NoteContextAwareWidget {
isEnabled() {
return this.note;
}
getTitle() {
return {
show: this.isEnabled(),
title: 'Link Map',
icon: 'bx bx-network-chart'
};
}
doRender() {
this.$widget = $(TPL);
2021-05-31 21:31:07 +02:00
this.$container = this.$widget.find(".link-map-container");
2021-05-31 21:20:30 +02:00
2021-05-31 23:38:47 +02:00
this.openState = 'small';
2021-05-31 21:20:30 +02:00
this.$openFullButton = this.$widget.find('.open-full-button');
this.$openFullButton.on('click', () => {
2021-05-31 23:38:47 +02:00
this.setFullHeight();
2021-05-31 21:20:30 +02:00
this.$openFullButton.hide();
this.$collapseButton.show();
2021-05-31 23:38:47 +02:00
this.openState = 'full';
2021-05-31 21:20:30 +02:00
});
this.$collapseButton = this.$widget.find('.collapse-button');
this.$collapseButton.on('click', () => {
2021-05-31 23:38:47 +02:00
this.setSmallSize();
2021-05-31 21:20:30 +02:00
this.$openFullButton.show();
this.$collapseButton.hide();
2021-05-31 23:38:47 +02:00
this.openState = 'small';
2021-05-31 21:20:30 +02:00
});
2021-05-28 23:19:11 +02:00
this.overflowing();
2021-05-31 23:38:47 +02:00
window.addEventListener('resize', () => {
if (!this.graph) { // no graph has been even rendered
return;
}
if (this.openState === 'full') {
this.setFullHeight();
}
else if (this.openState === 'small') {
this.setSmallSize();
}
}, false);
}
setSmallSize() {
const SMALL_SIZE_HEIGHT = 300;
const width = this.$widget.width();
this.$widget.find('.link-map-container')
.css("height", SMALL_SIZE_HEIGHT)
.css("width", width);
this.graph
.height(SMALL_SIZE_HEIGHT)
.width(width);
}
setFullHeight() {
const {top} = this.$widget[0].getBoundingClientRect();
const height = $(window).height() - top;
const width = this.$widget.width();
this.$widget.find('.link-map-container')
.css("height", height)
.css("width", this.$widget.width());
this.graph
.height(height)
.width(width);
2021-05-28 23:19:11 +02:00
}
2021-05-31 21:31:07 +02:00
setZoomLevel(level) {
this.zoomLevel = level;
}
2021-05-28 23:19:11 +02:00
async refreshWithNote(note) {
2021-05-31 21:31:07 +02:00
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)
2021-05-31 23:38:47 +02:00
.maxZoom(7)
2021-05-31 21:31:07 +02:00
.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} - <strong>${l.name}</strong> - ${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);
}
2021-05-28 23:19:11 +02:00
2021-05-31 21:31:07 +02:00
async nodeClicked(node) {
if (!node.expanded) {
const neighborGraph = await fetchNeighborGraph(node.id);
2021-05-28 23:19:11 +02:00
2021-05-31 21:31:07 +02:00
addToTasGraph(neighborGraph);
renderData(getTasGraph(), false);
}
}
2021-05-28 23:19:11 +02:00
2021-05-31 21:31:07 +02:00
async loadNotesAndRelations(options = {}) {
2021-05-31 23:38:47 +02:00
const {noteIdToLinkCountMap, links} = await server.post(`notes/${this.note.noteId}/link-map`, {
2021-05-31 21:31:07 +02:00
maxNotes: 30,
2021-05-31 23:38:47 +02:00
maxDepth: 1
2021-05-28 23:19:11 +02:00
});
2021-05-31 23:38:47 +02:00
// preload all notes
const notes = await froca.getNotes(Object.keys(noteIdToLinkCountMap), true);
const noteIdToLinkMap = {};
2021-05-31 21:31:07 +02:00
2021-05-31 23:38:47 +02:00
for (const link of links) {
noteIdToLinkMap[link.sourceNoteId] = noteIdToLinkMap[link.sourceNoteId] || [];
noteIdToLinkMap[link.sourceNoteId].push(link);
noteIdToLinkMap[link.targetNoteId] = noteIdToLinkMap[link.targetNoteId] || [];
noteIdToLinkMap[link.targetNoteId].push(link);
2021-05-31 21:31:07 +02:00
}
2021-05-31 23:38:47 +02:00
console.log(notes.map(note => ({
id: note.noteId,
name: note.title,
type: note.type,
expanded: noteIdToLinkCountMap[note.noteId] === noteIdToLinkMap[note.noteId].length
})))
2021-05-31 21:31:07 +02:00
return {
nodes: notes.map(note => ({
id: note.noteId,
name: note.title,
2021-05-31 23:38:47 +02:00
type: note.type,
expanded: noteIdToLinkCountMap[note.noteId] === noteIdToLinkMap[note.noteId].length
2021-05-31 21:31:07 +02:00
})),
links: links.map(link => ({
2021-05-31 23:38:47 +02:00
id: link.sourceNoteId + "-" + link.name + "-" + link.targetNoteId,
source: link.sourceNoteId,
2021-05-31 21:31:07 +02:00
target: link.targetNoteId,
name: link.name
}))
};
2021-05-28 23:19:11 +02:00
}
2021-05-31 21:31:07 +02:00
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) {
2021-05-31 23:38:47 +02:00
ctx.fillStyle = "white";
2021-05-31 21:31:07 +02:00
ctx.font = 10 + 'px MontserratLight';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
2021-05-31 23:38:47 +02:00
ctx.fillText("+", x, y + 0.5);
2021-05-31 21:31:07 +02:00
}
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);
2021-05-28 23:19:11 +02:00
}
2021-05-31 21:31:07 +02:00
return colour;
2021-05-28 23:19:11 +02:00
}
entitiesReloadedEvent({loadResults}) {
if (loadResults.getAttributes().find(attr => attr.type === 'relation' && (attr.noteId === this.noteId || attr.value === this.noteId))) {
this.noteSwitched();
}
const changedNoteIds = loadResults.getNoteIds();
if (changedNoteIds.length > 0) {
const $linkMapContainer = this.$widget.find('.link-map-container');
for (const noteId of changedNoteIds) {
const note = froca.notes[noteId];
if (note) {
$linkMapContainer.find(`a[data-note-path="${noteId}"]`).text(note.title);
}
}
}
}
}