mirror of
https://github.com/zadam/trilium.git
synced 2025-03-01 14:22:32 +01:00
link map WIP
This commit is contained in:
parent
bdff1c1246
commit
e48bbe5b19
@ -56,6 +56,10 @@ class Attribute extends AbstractEntity {
|
|||||||
|| (this.type === 'relation' && this.name === 'template');
|
|| (this.type === 'relation' && this.name === 'template');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get targetNoteId() { // alias
|
||||||
|
return this.type === 'relation' ? this.value : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
isAutoLink() {
|
isAutoLink() {
|
||||||
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
|
return this.type === 'relation' && ['internalLink', 'imageLink', 'relationMapLink', 'includeNoteLink'].includes(this.name);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
import libraryLoader from "./library_loader.js";
|
|
||||||
import server from "./server.js";
|
|
||||||
import froca from "./froca.js";
|
|
||||||
|
|
||||||
export default class LinkMap {
|
|
||||||
|
|
||||||
}
|
|
@ -46,31 +46,70 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
|
|||||||
this.$widget = $(TPL);
|
this.$widget = $(TPL);
|
||||||
this.$container = this.$widget.find(".link-map-container");
|
this.$container = this.$widget.find(".link-map-container");
|
||||||
|
|
||||||
|
this.openState = 'small';
|
||||||
|
|
||||||
this.$openFullButton = this.$widget.find('.open-full-button');
|
this.$openFullButton = this.$widget.find('.open-full-button');
|
||||||
this.$openFullButton.on('click', () => {
|
this.$openFullButton.on('click', () => {
|
||||||
const {top} = this.$widget[0].getBoundingClientRect();
|
this.setFullHeight();
|
||||||
|
|
||||||
const maxHeight = $(window).height() - top;
|
|
||||||
|
|
||||||
this.$widget.find('.link-map-container').css("height", maxHeight);
|
|
||||||
|
|
||||||
this.graph.height(maxHeight);
|
|
||||||
|
|
||||||
this.$openFullButton.hide();
|
this.$openFullButton.hide();
|
||||||
this.$collapseButton.show();
|
this.$collapseButton.show();
|
||||||
|
|
||||||
|
this.openState = 'full';
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$collapseButton = this.$widget.find('.collapse-button');
|
this.$collapseButton = this.$widget.find('.collapse-button');
|
||||||
this.$collapseButton.on('click', () => {
|
this.$collapseButton.on('click', () => {
|
||||||
this.$widget.find('.link-map-container,.force-graph-container,canvas').css("height", 300);
|
this.setSmallSize();
|
||||||
|
|
||||||
this.graph.height(300);
|
|
||||||
|
|
||||||
this.$openFullButton.show();
|
this.$openFullButton.show();
|
||||||
this.$collapseButton.hide();
|
this.$collapseButton.hide();
|
||||||
|
|
||||||
|
this.openState = 'small';
|
||||||
});
|
});
|
||||||
|
|
||||||
this.overflowing();
|
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) {
|
setZoomLevel(level) {
|
||||||
@ -90,7 +129,7 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
|
|||||||
.nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
|
.nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
|
||||||
.nodePointerAreaPaint((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)
|
.nodeLabel(node => node.name)
|
||||||
.maxZoom(5)
|
.maxZoom(7)
|
||||||
.nodePointerAreaPaint((node, color, ctx) => {
|
.nodePointerAreaPaint((node, color, ctx) => {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = color;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
@ -143,29 +182,41 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async loadNotesAndRelations(options = {}) {
|
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,
|
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) {
|
const noteIdToLinkMap = {};
|
||||||
noteIds.add(this.note.noteId);
|
|
||||||
|
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
|
console.log(notes.map(note => ({
|
||||||
const notes = await froca.getNotes(Array.from(noteIds), true);
|
id: note.noteId,
|
||||||
|
name: note.title,
|
||||||
|
type: note.type,
|
||||||
|
expanded: noteIdToLinkCountMap[note.noteId] === noteIdToLinkMap[note.noteId].length
|
||||||
|
})))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nodes: notes.map(note => ({
|
nodes: notes.map(note => ({
|
||||||
id: note.noteId,
|
id: note.noteId,
|
||||||
name: note.title,
|
name: note.title,
|
||||||
type: note.type
|
type: note.type,
|
||||||
|
expanded: noteIdToLinkCountMap[note.noteId] === noteIdToLinkMap[note.noteId].length
|
||||||
})),
|
})),
|
||||||
links: links.map(link => ({
|
links: links.map(link => ({
|
||||||
id: link.noteId + "-" + link.name + "-" + link.targetNoteId,
|
id: link.sourceNoteId + "-" + link.name + "-" + link.targetNoteId,
|
||||||
source: link.noteId,
|
source: link.sourceNoteId,
|
||||||
target: link.targetNoteId,
|
target: link.targetNoteId,
|
||||||
name: link.name
|
name: link.name
|
||||||
}))
|
}))
|
||||||
@ -219,11 +270,11 @@ export default class LinkMapWidget extends NoteContextAwareWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!node.expanded) {
|
if (!node.expanded) {
|
||||||
ctx.fillStyle = color;
|
ctx.fillStyle = "white";
|
||||||
ctx.font = 10 + 'px MontserratLight';
|
ctx.font = 10 + 'px MontserratLight';
|
||||||
ctx.textAlign = 'center';
|
ctx.textAlign = 'center';
|
||||||
ctx.textBaseline = 'middle';
|
ctx.textBaseline = 'middle';
|
||||||
ctx.fillText("+", x, y + 1);
|
ctx.fillText("+", x, y + 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.fillStyle = "#555";
|
ctx.fillStyle = "#555";
|
||||||
|
@ -1,78 +1,81 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const sql = require('../../services/sql');
|
const becca = require("../../becca/becca");
|
||||||
|
|
||||||
function getRelations(noteIds) {
|
function getRelations(noteId) {
|
||||||
noteIds = Array.from(noteIds);
|
const note = becca.getNote(noteId);
|
||||||
|
|
||||||
return [
|
if (!note) {
|
||||||
// first read all relations
|
throw new Error(noteId);
|
||||||
// some "system" relations are not included since they are rarely useful to see (#1820)
|
}
|
||||||
...sql.getManyRows(`
|
|
||||||
SELECT noteId, name, value AS targetNoteId
|
const allRelations = note.getOwnedRelations().concat(note.getTargetRelations());
|
||||||
FROM attributes
|
|
||||||
WHERE (noteId IN (???) OR value IN (???))
|
return allRelations.filter(rel => {
|
||||||
AND type = 'relation'
|
if (rel.name === 'relationMapLink' || rel.name === 'template') {
|
||||||
AND name NOT IN ('imageLink', 'relationMapLink', 'template')
|
return false;
|
||||||
AND isDeleted = 0
|
}
|
||||||
AND noteId != ''
|
else if (rel.name === 'imageLink') {
|
||||||
AND value != ''`, noteIds),
|
const parentNote = becca.getNote(rel.noteId);
|
||||||
// ... 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
|
return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value);
|
||||||
...sql.getManyRows(`
|
}
|
||||||
SELECT rel.noteId, rel.name, rel.value AS targetNoteId
|
else {
|
||||||
FROM attributes AS rel
|
return true;
|
||||||
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
|
function collectRelations(noteId, relations, depth) {
|
||||||
AND rel.noteId != ''
|
if (depth === 0) {
|
||||||
AND rel.value != ''
|
return;
|
||||||
AND branches.branchId IS NULL`, noteIds)
|
}
|
||||||
];
|
|
||||||
|
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) {
|
function getLinkMap(req) {
|
||||||
const {noteId} = req.params;
|
const {noteId} = req.params;
|
||||||
const {maxNotes, maxDepth} = req.body;
|
const {maxDepth} = req.body;
|
||||||
|
|
||||||
let noteIds = new Set([noteId]);
|
let relations = new Set();
|
||||||
let relations;
|
|
||||||
|
|
||||||
let depth = 0;
|
collectRelations(noteId, relations, maxDepth);
|
||||||
|
|
||||||
while (noteIds.size < maxNotes) {
|
relations = Array.from(relations);
|
||||||
relations = getRelations(noteIds);
|
|
||||||
|
|
||||||
if (depth === maxDepth) {
|
const noteIds = new Set(relations.map(rel => rel.noteId)
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
let newNoteIds = relations.map(rel => rel.noteId)
|
|
||||||
.concat(relations.map(rel => rel.targetNoteId))
|
.concat(relations.map(rel => rel.targetNoteId))
|
||||||
.filter(noteId => !noteIds.has(noteId));
|
.concat([noteId]));
|
||||||
|
|
||||||
if (newNoteIds.length === 0) {
|
const noteIdToLinkCountMap = {};
|
||||||
// no new note discovered, no need to search any further
|
|
||||||
break;
|
for (const noteId of noteIds) {
|
||||||
|
noteIdToLinkCountMap[noteId] = getRelations(noteId).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const newNoteId of newNoteIds) {
|
return {
|
||||||
noteIds.add(newNoteId);
|
noteIdToLinkCountMap,
|
||||||
|
links: Array.from(relations).map(rel => ({
|
||||||
if (noteIds.size >= maxNotes) {
|
sourceNoteId: rel.noteId,
|
||||||
break;
|
targetNoteId: rel.value,
|
||||||
}
|
name: rel.name
|
||||||
}
|
}))
|
||||||
|
};
|
||||||
depth++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user