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() {
utils.setCookie('trilium-device', 'desktop');
utils.reloadFrontendApp();
utils.reloadFrontendApp("Switching to desktop version");
}
async openInWindowCommand({notePath, hoistedNoteId}) {

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

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

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

@ -1,4 +1,8 @@
function reloadFrontendApp() {
function reloadFrontendApp(reason) {
if (reason) {
logInfo("Frontend app reload: " + reason);
}
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.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);

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

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

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

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