global link map WIP

This commit is contained in:
zadam 2021-09-17 22:34:23 +02:00
parent 43e829ca99
commit a0caa21458
12 changed files with 163 additions and 38 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -169,7 +169,7 @@ export default class Entrypoints extends Component {
async switchToDesktopVersionCommand() { async switchToDesktopVersionCommand() {
utils.setCookie('trilium-device', 'desktop'); utils.setCookie('trilium-device', 'desktop');
utils.reloadFrontendApp(); utils.reloadFrontendApp("Switching to desktop version");
} }
async openInWindowCommand({notePath, hoistedNoteId}) { async openInWindowCommand({notePath, hoistedNoteId}) {

View File

@ -88,7 +88,7 @@ function processNoteChange(loadResults, ec) {
loadResults.addNote(ec.entityId, ec.sourceId); loadResults.addNote(ec.entityId, ec.sourceId);
if (ec.isErased && ec.entityId in froca.notes) { if (ec.isErased && ec.entityId in froca.notes) {
utils.reloadFrontendApp(); utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
return; return;
} }
@ -102,7 +102,7 @@ function processNoteChange(loadResults, ec) {
function processBranchChange(loadResults, ec) { function processBranchChange(loadResults, ec) {
if (ec.isErased && ec.entityId in froca.branches) { if (ec.isErased && ec.entityId in froca.branches) {
utils.reloadFrontendApp(); utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
return; return;
} }
@ -180,7 +180,7 @@ function processAttributeChange(loadResults, ec) {
let attribute = froca.attributes[ec.entityId]; let attribute = froca.attributes[ec.entityId];
if (ec.isErased && ec.entityId in froca.attributes) { if (ec.isErased && ec.entityId in froca.attributes) {
utils.reloadFrontendApp(); utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
return; return;
} }

View File

@ -3,7 +3,6 @@ import appContext from "./app_context.js";
import server from "./server.js"; import server from "./server.js";
import libraryLoader from "./library_loader.js"; import libraryLoader from "./library_loader.js";
import ws from "./ws.js"; import ws from "./ws.js";
import protectedSessionHolder from "./protected_session_holder.js";
import froca from "./froca.js"; import froca from "./froca.js";
function setupGlobs() { function setupGlobs() {

View File

@ -69,7 +69,7 @@ ws.subscribeToMessages(async message => {
toastService.showMessage("Protected session has been started."); toastService.showMessage("Protected session has been started.");
} }
else if (message.type === 'protectedSessionLogout') { else if (message.type === 'protectedSessionLogout') {
utils.reloadFrontendApp(); utils.reloadFrontendApp(`Protected session logout`);
} }
}); });

View File

@ -1,4 +1,8 @@
function reloadFrontendApp() { function reloadFrontendApp(reason) {
if (reason) {
logInfo("Frontend app reload: " + reason);
}
window.location.reload(true); window.location.reload(true);
} }

View File

@ -25,7 +25,19 @@ function logError(message) {
} }
} }
function logInfo(message) {
console.log(utils.now(), message);
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({
type: 'log-info',
info: message
}));
}
}
window.logError = logError; window.logError = logError;
window.logInfo = logInfo;
function subscribeToMessages(messageHandler) { function subscribeToMessages(messageHandler) {
messageHandlers.push(messageHandler); messageHandlers.push(messageHandler);
@ -91,7 +103,7 @@ async function handleMessage(event) {
} }
if (message.type === 'reload-frontend') { if (message.type === 'reload-frontend') {
utils.reloadFrontendApp(); utils.reloadFrontendApp("received request from backend to reload frontend");
} }
else if (message.type === 'frontend-update') { else if (message.type === 'frontend-update') {
await executeFrontendUpdate(message.data.entityChanges); await executeFrontendUpdate(message.data.entityChanges);

View File

@ -1,7 +1,6 @@
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.js"; import libraryLoader from "../../services/library_loader.js";
import server from "../../services/server.js"; import server from "../../services/server.js";
import froca from "../../services/froca.js";
const TPL = `<div class="note-detail-global-link-map note-detail-printable"> const TPL = `<div class="note-detail-global-link-map note-detail-printable">
<style> <style>
@ -56,7 +55,9 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
.width(this.$container.width()) .width(this.$container.width())
.height(this.$container.height()) .height(this.$container.height())
.onZoom(zoom => this.setZoomLevel(zoom.k)) .onZoom(zoom => this.setZoomLevel(zoom.k))
.nodeRelSize(7) .d3AlphaDecay(0.01)
.d3VelocityDecay(0.08)
.nodeRelSize(node => this.noteIdToSizeMap[node.id])
.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)
@ -70,19 +71,23 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
.linkLabel(l => `${l.source.name} - <strong>${l.name}</strong> - ${l.target.name}`) .linkLabel(l => `${l.source.name} - <strong>${l.name}</strong> - ${l.target.name}`)
.linkCanvasObject((link, ctx) => this.paintLink(link, ctx)) .linkCanvasObject((link, ctx) => this.paintLink(link, ctx))
.linkCanvasObjectMode(() => "after") .linkCanvasObjectMode(() => "after")
.linkDirectionalArrowLength(4) .warmupTicks(10)
// .linkDirectionalArrowLength(5)
.linkDirectionalArrowRelPos(1) .linkDirectionalArrowRelPos(1)
.linkWidth(2) .linkWidth(1)
.linkColor(() => this.css.mutedTextColor) .linkColor(() => this.css.mutedTextColor)
.d3VelocityDecay(0.2) // .d3VelocityDecay(0.2)
// .dagMode("radialout")
.onNodeClick(node => this.nodeClicked(node)); .onNodeClick(node => this.nodeClicked(node));
this.graph.d3Force('link').distance(50); this.graph.d3Force('link').distance(5);
//
this.graph.d3Force('center').strength(0.9); this.graph.d3Force('center').strength(0.01);
//
this.graph.d3Force('charge').strength(-30); this.graph.d3Force('charge').strength(-30);
this.graph.d3Force('charge').distanceMax(400);
this.graph.d3Force('charge').distanceMax(1000);
this.renderData(await this.loadNotesAndRelations()); this.renderData(await this.loadNotesAndRelations());
} }
@ -113,13 +118,18 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
paintNode(node, color, ctx) { paintNode(node, color, ctx) {
const {x, y} = node; const {x, y} = node;
const size = this.noteIdToSizeMap[node.id];
ctx.fillStyle = node.id === this.noteId ? 'red' : color; ctx.fillStyle = node.id === this.noteId ? 'red' : color;
ctx.beginPath(); ctx.beginPath();
ctx.arc(x, y, node.id === this.noteId ? 8 : 4, 0, 2 * Math.PI, false); ctx.arc(x, y, size, 0, 2 * Math.PI, false);
ctx.fill(); ctx.fill();
if (this.zoomLevel < 2) { const toRender = this.zoomLevel > 2
|| (this.zoomLevel > 1 && size > 6)
|| (this.zoomLevel > 0.3 && size > 10);
if (!toRender) {
return; return;
} }
@ -132,7 +142,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
} }
ctx.fillStyle = this.css.textColor; ctx.fillStyle = this.css.textColor;
ctx.font = 5 + 'px ' + this.css.fontFamily; ctx.font = size + 'px ' + this.css.fontFamily;
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'middle'; ctx.textBaseline = 'middle';
@ -142,7 +152,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
title = title.substr(0, 15) + "..."; title = title.substr(0, 15) + "...";
} }
ctx.fillText(title, x, y + (node.id === this.noteId ? 11 : 7)); ctx.fillText(title, x, y + Math.round(size * 1.5));
} }
paintLink(link, ctx) { paintLink(link, ctx) {
@ -183,20 +193,16 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
this.linkIdToLinkMap = {}; this.linkIdToLinkMap = {};
this.noteIdToLinkCountMap = {}; this.noteIdToLinkCountMap = {};
const resp = await server.post(`notes/root/link-map`, { const resp = await server.post(`global-link-map`);
maxNotes: 1000,
maxDepth
});
this.noteIdToLinkCountMap = {...this.noteIdToLinkCountMap, ...resp.noteIdToLinkCountMap}; this.noteIdToLinkCountMap = resp.noteIdToLinkCountMap;
this.calculateSizes(resp.noteIdToDescendantCountMap);
for (const link of resp.links) { for (const link of resp.links) {
this.linkIdToLinkMap[link.id] = link; this.linkIdToLinkMap[link.id] = link;
} }
// preload all notes
const notes = await froca.getNotes(Object.keys(this.noteIdToLinkCountMap), true);
const noteIdToLinkIdMap = {}; const noteIdToLinkIdMap = {};
noteIdToLinkIdMap[this.noteId] = new Set(); // for case there are no relations noteIdToLinkIdMap[this.noteId] = new Set(); // for case there are no relations
const linksGroupedBySourceTarget = {}; const linksGroupedBySourceTarget = {};
@ -226,11 +232,11 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
} }
return { return {
nodes: notes.map(note => ({ nodes: resp.notes.map(([noteId, title, type]) => ({
id: note.noteId, id: noteId,
name: note.title, name: title,
type: note.type, type: type,
expanded: this.noteIdToLinkCountMap[note.noteId] === noteIdToLinkIdMap[note.noteId].size expanded: true
})), })),
links: Object.values(linksGroupedBySourceTarget).map(link => ({ links: Object.values(linksGroupedBySourceTarget).map(link => ({
id: link.id, id: link.id,
@ -241,6 +247,20 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
}; };
} }
calculateSizes(noteIdToDescendantCountMap) {
this.noteIdToSizeMap = {};
for (const noteId in noteIdToDescendantCountMap) {
this.noteIdToSizeMap[noteId] = 4;
const count = noteIdToDescendantCountMap[noteId];
if (count > 0) {
this.noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5));
}
}
}
renderData(data, zoomToFit = true, zoomPadding = 10) { renderData(data, zoomToFit = true, zoomPadding = 10) {
this.graph.graphData(data); this.graph.graphData(data);

View File

@ -79,6 +79,92 @@ function getLinkMap(req) {
}; };
} }
function buildDescendantCountMap() {
const noteIdToCountMap = {};
function getCount(noteId) {
if (!(noteId in noteIdToCountMap)) {
const note = becca.getNote(noteId);
noteIdToCountMap[noteId] = note.children.length;
for (const child of note.children) {
noteIdToCountMap[noteId] += getCount(child.noteId);
}
}
return noteIdToCountMap[noteId];
}
getCount('root');
return noteIdToCountMap;
}
function getGlobalLinkMap() {
const relations = Object.values(becca.attributes).filter(rel => {
if (rel.type !== 'relation' || 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;
}
});
const noteIdToLinkCountMap = {};
for (const noteId in becca.notes) {
noteIdToLinkCountMap[noteId] = getRelations(noteId).length;
}
let links = Array.from(relations).map(rel => ({
id: rel.noteId + "-" + rel.name + "-" + rel.value,
sourceNoteId: rel.noteId,
targetNoteId: rel.value,
name: rel.name
}));
links = [];
const noteIds = new Set();
const notes = Object.values(becca.notes)
.filter(note => !note.isArchived)
.map(note => [
note.noteId,
note.isContentAvailable() ? note.title : '[protected]',
note.type
]);
notes.forEach(([noteId]) => noteIds.add(noteId));
for (const branch of Object.values(becca.branches)) {
if (!noteIds.has(branch.parentNoteId) || !noteIds.has(branch.noteId)) {
continue;
}
links.push({
id: branch.branchId,
sourceNoteId: branch.parentNoteId,
targetNoteId: branch.noteId,
name: 'branch'
});
}
return {
notes: notes,
noteIdToLinkCountMap,
noteIdToDescendantCountMap: buildDescendantCountMap(),
links: links
};
}
module.exports = { module.exports = {
getLinkMap getLinkMap,
getGlobalLinkMap
}; };

View File

@ -221,6 +221,7 @@ function register(app) {
apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap); apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap);
apiRoute(POST, '/api/global-link-map', linkMapRoute.getGlobalLinkMap);
apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote);
apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote); apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote);

View File

@ -41,6 +41,9 @@ function init(httpServer, sessionParser) {
if (message.type === 'log-error') { if (message.type === 'log-error') {
log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack); log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack);
} }
else if (message.type === 'log-info') {
log.info('JS Info: ' + message.info);
}
else if (message.type === 'ping') { else if (message.type === 'ping') {
await syncMutexService.doExclusively(() => sendPing(ws)); await syncMutexService.doExclusively(() => sendPing(ws));
} }