link map WIP

This commit is contained in:
zadam 2021-05-31 23:38:47 +02:00
parent bdff1c1246
commit e48bbe5b19
4 changed files with 140 additions and 89 deletions

View File

@ -56,6 +56,10 @@ class Attribute extends AbstractEntity {
|| (this.type === 'relation' && this.name === 'template');
}
get targetNoteId() { // alias
return this.type === 'relation' ? this.value : undefined;
}
isAutoLink() {
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
}

View File

@ -1,7 +0,0 @@
import libraryLoader from "./library_loader.js";
import server from "./server.js";
import froca from "./froca.js";
export default class LinkMap {
}

View File

@ -46,31 +46,70 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
this.$widget = $(TPL);
this.$container = this.$widget.find(".link-map-container");
this.openState = 'small';
this.$openFullButton = this.$widget.find('.open-full-button');
this.$openFullButton.on('click', () => {
const {top} = this.$widget[0].getBoundingClientRect();
const maxHeight = $(window).height() - top;
this.$widget.find('.link-map-container').css("height", maxHeight);
this.graph.height(maxHeight);
this.setFullHeight();
this.$openFullButton.hide();
this.$collapseButton.show();
this.openState = 'full';
});
this.$collapseButton = this.$widget.find('.collapse-button');
this.$collapseButton.on('click', () => {
this.$widget.find('.link-map-container,.force-graph-container,canvas').css("height", 300);
this.graph.height(300);
this.setSmallSize();
this.$openFullButton.show();
this.$collapseButton.hide();
this.openState = 'small';
});
this.overflowing();
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);
}
setZoomLevel(level) {
@ -90,7 +129,7 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
.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)
.maxZoom(7)
.nodePointerAreaPaint((node, color, ctx) => {
ctx.fillStyle = color;
ctx.beginPath();
@ -143,29 +182,41 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
}
async loadNotesAndRelations(options = {}) {
const links = await server.post(`notes/${this.note.noteId}/link-map`, {
const {noteIdToLinkCountMap, links} = await server.post(`notes/${this.note.noteId}/link-map`, {
maxNotes: 30,
maxDepth: 5
maxDepth: 1
});
const noteIds = new Set(links.map(l => l.noteId).concat(links.map(l => l.targetNoteId)));
// preload all notes
const notes = await froca.getNotes(Object.keys(noteIdToLinkCountMap), true);
if (noteIds.size === 0) {
noteIds.add(this.note.noteId);
const noteIdToLinkMap = {};
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);
}
// preload all notes
const notes = await froca.getNotes(Array.from(noteIds), true);
console.log(notes.map(note => ({
id: note.noteId,
name: note.title,
type: note.type,
expanded: noteIdToLinkCountMap[note.noteId] === noteIdToLinkMap[note.noteId].length
})))
return {
nodes: notes.map(note => ({
id: note.noteId,
name: note.title,
type: note.type
type: note.type,
expanded: noteIdToLinkCountMap[note.noteId] === noteIdToLinkMap[note.noteId].length
})),
links: links.map(link => ({
id: link.noteId + "-" + link.name + "-" + link.targetNoteId,
source: link.noteId,
id: link.sourceNoteId + "-" + link.name + "-" + link.targetNoteId,
source: link.sourceNoteId,
target: link.targetNoteId,
name: link.name
}))
@ -219,11 +270,11 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
}
if (!node.expanded) {
ctx.fillStyle = color;
ctx.fillStyle = "white";
ctx.font = 10 + 'px MontserratLight';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText("+", x, y + 1);
ctx.fillText("+", x, y + 0.5);
}
ctx.fillStyle = "#555";

View File

@ -1,78 +1,81 @@
"use strict";
const sql = require('../../services/sql');
const becca = require("../../becca/becca");
function getRelations(noteIds) {
noteIds = Array.from(noteIds);
function getRelations(noteId) {
const note = becca.getNote(noteId);
return [
// first read all relations
// some "system" relations are not included since they are rarely useful to see (#1820)
...sql.getManyRows(`
SELECT noteId, name, value AS targetNoteId
FROM attributes
WHERE (noteId IN (???) OR value IN (???))
AND type = 'relation'
AND name NOT IN ('imageLink', 'relationMapLink', 'template')
AND isDeleted = 0
AND noteId != ''
AND value != ''`, noteIds),
// ... then read only imageLink relations which are not connecting parent and child
// this is done to not show image links in the trivial case where they are direct children of the note to which they are included. Same heuristic as in note tree
...sql.getManyRows(`
SELECT rel.noteId, rel.name, rel.value AS targetNoteId
FROM attributes AS rel
LEFT JOIN branches ON branches.parentNoteId = rel.noteId AND branches.noteId = rel.value AND branches.isDeleted = 0
WHERE (rel.noteId IN (???) OR rel.value IN (???))
AND rel.type = 'relation'
AND rel.name = 'imageLink'
AND rel.isDeleted = 0
AND rel.noteId != ''
AND rel.value != ''
AND branches.branchId IS NULL`, noteIds)
];
if (!note) {
throw new Error(noteId);
}
const allRelations = note.getOwnedRelations().concat(note.getTargetRelations());
return allRelations.filter(rel => {
if (rel.name === 'relationMapLink' || rel.name === 'template') {
return false;
}
else if (rel.name === 'imageLink') {
const parentNote = becca.getNote(rel.noteId);
return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value);
}
else {
return true;
}
});
}
function collectRelations(noteId, relations, depth) {
if (depth === 0) {
return;
}
for (const relation of getRelations(noteId)) {
if (!relations.has(relation)) {
if (!relation.value) {
continue;
}
relations.add(relation);
if (relation.noteId !== noteId) {
collectRelations(relation.noteId, relations, depth--);
} else if (relation.value !== noteId) {
collectRelations(relation.value, relations, depth--);
}
}
}
}
function getLinkMap(req) {
const {noteId} = req.params;
const {maxNotes, maxDepth} = req.body;
const {maxDepth} = req.body;
let noteIds = new Set([noteId]);
let relations;
let relations = new Set();
let depth = 0;
collectRelations(noteId, relations, maxDepth);
while (noteIds.size < maxNotes) {
relations = getRelations(noteIds);
relations = Array.from(relations);
if (depth === maxDepth) {
break;
}
const noteIds = new Set(relations.map(rel => rel.noteId)
.concat(relations.map(rel => rel.targetNoteId))
.concat([noteId]));
let newNoteIds = relations.map(rel => rel.noteId)
.concat(relations.map(rel => rel.targetNoteId))
.filter(noteId => !noteIds.has(noteId));
const noteIdToLinkCountMap = {};
if (newNoteIds.length === 0) {
// no new note discovered, no need to search any further
break;
}
for (const newNoteId of newNoteIds) {
noteIds.add(newNoteId);
if (noteIds.size >= maxNotes) {
break;
}
}
depth++;
for (const noteId of noteIds) {
noteIdToLinkCountMap[noteId] = getRelations(noteId).length;
}
// keep only links coming from and targetting some note in the noteIds set
relations = relations.filter(rel => noteIds.has(rel.noteId) && noteIds.has(rel.targetNoteId));
return relations;
return {
noteIdToLinkCountMap,
links: Array.from(relations).map(rel => ({
sourceNoteId: rel.noteId,
targetNoteId: rel.value,
name: rel.name
}))
};
}
module.exports = {