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() {
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));
}