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

View File

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

View File

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

View File

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

View File

@@ -69,7 +69,7 @@ ws.subscribeToMessages(async message => {
toastService.showMessage("Protected session has been started.");
}
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);
}

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.logInfo = logInfo;
function subscribeToMessages(messageHandler) {
messageHandlers.push(messageHandler);
@@ -91,7 +103,7 @@ async function handleMessage(event) {
}
if (message.type === 'reload-frontend') {
utils.reloadFrontendApp();
utils.reloadFrontendApp("received request from backend to reload frontend");
}
else if (message.type === 'frontend-update') {
await executeFrontendUpdate(message.data.entityChanges);

View File

@@ -1,7 +1,6 @@
import TypeWidget from "./type_widget.js";
import libraryLoader from "../../services/library_loader.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">
<style>
@@ -56,7 +55,9 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
.width(this.$container.width())
.height(this.$container.height())
.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))
.nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx))
.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}`)
.linkCanvasObject((link, ctx) => this.paintLink(link, ctx))
.linkCanvasObjectMode(() => "after")
.linkDirectionalArrowLength(4)
.warmupTicks(10)
// .linkDirectionalArrowLength(5)
.linkDirectionalArrowRelPos(1)
.linkWidth(2)
.linkWidth(1)
.linkColor(() => this.css.mutedTextColor)
.d3VelocityDecay(0.2)
// .d3VelocityDecay(0.2)
// .dagMode("radialout")
.onNodeClick(node => this.nodeClicked(node));
this.graph.d3Force('link').distance(50);
this.graph.d3Force('center').strength(0.9);
this.graph.d3Force('link').distance(5);
//
this.graph.d3Force('center').strength(0.01);
//
this.graph.d3Force('charge').strength(-30);
this.graph.d3Force('charge').distanceMax(400);
this.graph.d3Force('charge').distanceMax(1000);
this.renderData(await this.loadNotesAndRelations());
}
@@ -113,13 +118,18 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
paintNode(node, color, ctx) {
const {x, y} = node;
const size = this.noteIdToSizeMap[node.id];
ctx.fillStyle = node.id === this.noteId ? 'red' : color;
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();
if (this.zoomLevel < 2) {
const toRender = this.zoomLevel > 2
|| (this.zoomLevel > 1 && size > 6)
|| (this.zoomLevel > 0.3 && size > 10);
if (!toRender) {
return;
}
@@ -132,7 +142,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
}
ctx.fillStyle = this.css.textColor;
ctx.font = 5 + 'px ' + this.css.fontFamily;
ctx.font = size + 'px ' + this.css.fontFamily;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
@@ -142,7 +152,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
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) {
@@ -183,20 +193,16 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
this.linkIdToLinkMap = {};
this.noteIdToLinkCountMap = {};
const resp = await server.post(`notes/root/link-map`, {
maxNotes: 1000,
maxDepth
});
const resp = await server.post(`global-link-map`);
this.noteIdToLinkCountMap = {...this.noteIdToLinkCountMap, ...resp.noteIdToLinkCountMap};
this.noteIdToLinkCountMap = resp.noteIdToLinkCountMap;
this.calculateSizes(resp.noteIdToDescendantCountMap);
for (const link of resp.links) {
this.linkIdToLinkMap[link.id] = link;
}
// preload all notes
const notes = await froca.getNotes(Object.keys(this.noteIdToLinkCountMap), true);
const noteIdToLinkIdMap = {};
noteIdToLinkIdMap[this.noteId] = new Set(); // for case there are no relations
const linksGroupedBySourceTarget = {};
@@ -226,11 +232,11 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget {
}
return {
nodes: notes.map(note => ({
id: note.noteId,
name: note.title,
type: note.type,
expanded: this.noteIdToLinkCountMap[note.noteId] === noteIdToLinkIdMap[note.noteId].size
nodes: resp.notes.map(([noteId, title, type]) => ({
id: noteId,
name: title,
type: type,
expanded: true
})),
links: Object.values(linksGroupedBySourceTarget).map(link => ({
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) {
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 = {
getLinkMap
getLinkMap,
getGlobalLinkMap
};

View File

@@ -221,6 +221,7 @@ function register(app) {
apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute);
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/date/:date', specialNotesRoute.getDateNote);

View File

@@ -41,6 +41,9 @@ function init(httpServer, sessionParser) {
if (message.type === 'log-error') {
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') {
await syncMutexService.doExclusively(() => sendPing(ws));
}