global link map WIP

pull/255/head
zadam 2021-09-17 22:34:23 +07:00
parent 43e829ca99
commit a0caa21458
12 changed files with 162 additions and 37 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -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}) {

@ -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;
} }

@ -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() {

@ -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`);
} }
}); });

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

@ -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);

@ -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.01);
//
this.graph.d3Force('charge').strength(-30);
this.graph.d3Force('center').strength(0.9);
this.graph.d3Force('charge').strength(-30); this.graph.d3Force('charge').distanceMax(1000);
this.graph.d3Force('charge').distanceMax(400);
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);

@ -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
}; };

@ -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);

@ -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));
} }