From a766374bf49410e4aeab957bcfa4f63085ca1faa Mon Sep 17 00:00:00 2001 From: zadam Date: Wed, 22 Sep 2021 22:25:39 +0200 Subject: [PATCH] unified note map with ribbon map --- src/public/app/widgets/note_map.js | 97 ++++--- .../app/widgets/ribbon_widgets/note_map.js | 256 +----------------- .../app/widgets/type_widgets/note_map.js | 4 +- 3 files changed, 72 insertions(+), 285 deletions(-) diff --git a/src/public/app/widgets/note_map.js b/src/public/app/widgets/note_map.js index e296a0d7c..ae1d80314 100644 --- a/src/public/app/widgets/note_map.js +++ b/src/public/app/widgets/note_map.js @@ -29,16 +29,25 @@ const TPL = `
+
+
`; export default class NoteMapWidget extends NoteContextAwareWidget { + constructor(widgetMode) { + super(); + + this.widgetMode = widgetMode; // 'type' or 'ribbon' + } + doRender() { this.$widget = $(TPL); this.$container = this.$widget.find(".note-map-container"); + this.$styleResolver = this.$widget.find('.style-resolver'); - window.addEventListener('resize', () => this.setFullHeight(), false); + window.addEventListener('resize', () => this.setHeight(), false); this.$widget.find(".map-type-switcher button").on("click", async e => { const type = $(e.target).closest("button").attr("data-type"); @@ -49,31 +58,30 @@ export default class NoteMapWidget extends NoteContextAwareWidget { super.doRender(); } - setFullHeight() { + setHeight() { if (!this.graph) { // no graph has been even rendered return; } - const {top} = this.$widget[0].getBoundingClientRect(); - - const height = $(window).height() - top; - const width = this.$widget.width(); - - this.$widget.find('.note-map-container') - .css("height", height) - .css("width", this.$widget.width()); + const $parent = this.$widget.parent(); this.graph - .height(height) - .width(width); + .height($parent.height()) + .width($parent.width()); } async refreshWithNote() { this.$widget.show(); + this.css = { + fontFamily: this.$container.css("font-family"), + textColor: this.rgb2hex(this.$container.css("color")), + mutedTextColor: this.rgb2hex(this.$styleResolver.css("color")) + }; + this.mapType = this.note.getLabelValue("mapType") === "tree" ? "tree" : "link"; - this.setFullHeight(); + this.setHeight(); await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH); @@ -98,7 +106,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget { .linkDirectionalArrowRelPos(1) .linkWidth(1) .linkColor(() => this.css.mutedTextColor) - .onNodeClick(node => this.nodeClicked(node)); + .onNodeClick(node => appContext.tabManager.getActiveContext().setNote(node.id)); if (this.mapType === 'link') { this.graph @@ -112,20 +120,29 @@ export default class NoteMapWidget extends NoteContextAwareWidget { this.graph.d3Force('charge').strength(-30); this.graph.d3Force('charge').distanceMax(1000); - let mapRootNoteId = this.note.getLabelValue("mapRootNoteId"); - - if (mapRootNoteId === 'hoisted') { - mapRootNoteId = hoistedNoteService.getHoistedNoteId(); - } - else if (!mapRootNoteId) { - mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId; - } + let mapRootNoteId = this.getMapRootNoteId(); const data = await this.loadNotesAndRelations(mapRootNoteId); this.renderData(data); } + getMapRootNoteId() { + if (this.widgetMode === 'ribbon') { + return this.noteId; + } + + let mapRootNoteId = this.note.getLabelValue("mapRootNoteId"); + + if (mapRootNoteId === 'hoisted') { + mapRootNoteId = hoistedNoteService.getHoistedNoteId(); + } else if (!mapRootNoteId) { + mapRootNoteId = appContext.tabManager.getActiveContext().parentNoteId; + } + + return mapRootNoteId; + } + stringToColor(str) { let hash = 0; for (let i = 0; i < str.length; i++) { @@ -167,14 +184,6 @@ export default class NoteMapWidget extends NoteContextAwareWidget { return; } - if (!node.expanded) { - ctx.fillStyle = this.css.textColor; - ctx.font = 10 + 'px ' + this.css.fontFamily; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText("+", x, y + 0.5); - } - ctx.fillStyle = this.css.textColor; ctx.font = size + 'px ' + this.css.fontFamily; ctx.textAlign = 'center'; @@ -265,13 +274,14 @@ export default class NoteMapWidget extends NoteContextAwareWidget { } } + this.nodes = resp.notes.map(([noteId, title, type]) => ({ + id: noteId, + name: title, + type: type, + })); + return { - nodes: resp.notes.map(([noteId, title, type]) => ({ - id: noteId, - name: title, - type: type, - expanded: true - })), + nodes: this.nodes, links: Object.values(linksGroupedBySourceTarget).map(link => ({ id: link.id, source: link.sourceNoteId, @@ -295,11 +305,20 @@ export default class NoteMapWidget extends NoteContextAwareWidget { } } - renderData(data, zoomToFit = true, zoomPadding = 10) { + renderData(data) { this.graph.graphData(data); - if (zoomToFit && data.nodes.length > 1) { - setTimeout(() => this.graph.zoomToFit(400, zoomPadding), 1000); + if (this.widgetMode === 'ribbon') { + setTimeout(() => { + const node = this.nodes.find(node => node.id === this.noteId); + + this.graph.centerAt(node.x, node.y, 500); + }, 1000); + } + else if (this.widgetMode === 'type') { + if (data.nodes.length > 1) { + setTimeout(() => this.graph.zoomToFit(400, 10), 1000); + } } } diff --git a/src/public/app/widgets/ribbon_widgets/note_map.js b/src/public/app/widgets/ribbon_widgets/note_map.js index 163cdede2..435ef390e 100644 --- a/src/public/app/widgets/ribbon_widgets/note_map.js +++ b/src/public/app/widgets/ribbon_widgets/note_map.js @@ -1,8 +1,5 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js"; -import froca from "../../services/froca.js"; -import libraryLoader from "../../services/library_loader.js"; -import server from "../../services/server.js"; -import appContext from "../../services/app_context.js"; +import NoteMapWidget from "../note_map.js"; const TPL = `
@@ -33,11 +30,16 @@ const TPL = `
- -
`; export default class NoteMapRibbonWidget extends NoteContextAwareWidget { + constructor() { + super(); + + this.noteMapWidget = new NoteMapWidget('ribbon'); + this.child(this.noteMapWidget); + } + get name() { return "noteMap"; } @@ -62,6 +64,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { this.$widget = $(TPL); this.contentSized(); this.$container = this.$widget.find(".note-map-container"); + this.$container.append(this.noteMapWidget.render()); this.openState = 'small'; @@ -73,6 +76,8 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { this.$collapseButton.show(); this.openState = 'full'; + + this.noteMapWidget.setHeight(); }); this.$collapseButton = this.$widget.find('.collapse-button'); @@ -83,11 +88,10 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { this.$collapseButton.hide(); this.openState = 'small'; + + this.noteMapWidget.setHeight(); }); - this.$styleResolver = this.$widget.find('.style-resolver'); - - window.addEventListener('resize', () => { if (!this.graph) { // no graph has been even rendered return; @@ -107,10 +111,6 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { const width = this.$widget.width(); this.$widget.find('.note-map-container') - .css("height", SMALL_SIZE_HEIGHT) - .css("width", width); - - this.graph .height(SMALL_SIZE_HEIGHT) .width(width); } @@ -122,237 +122,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget { const width = this.$widget.width(); this.$widget.find('.note-map-container') - .css("height", height) - .css("width", this.$widget.width()); - - this.graph .height(height) .width(width); } - - setZoomLevel(level) { - this.zoomLevel = level; - } - - async refreshWithNote(note) { - this.linkIdToLinkMap = {}; - this.noteIdToLinkCountMap = {}; - - this.$container.empty(); - - this.css = { - fontFamily: this.$container.css("font-family"), - textColor: this.rgb2hex(this.$container.css("color")), - mutedTextColor: this.rgb2hex(this.$styleResolver.css("color")) - }; - - await libraryLoader.requireLibrary(libraryLoader.FORCE_GRAPH); - - this.graph = ForceGraph()(this.$container[0]) - .width(this.$container.width()) - .height(this.$container.height()) - .onZoom(zoom => this.setZoomLevel(zoom.k)) - .nodeRelSize(7) - .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) - .maxZoom(7) - .nodePointerAreaPaint((node, color, ctx) => { - ctx.fillStyle = color; - ctx.beginPath(); - ctx.arc(node.x, node.y, 5, 0, 2 * Math.PI, false); - ctx.fill(); - }) - .linkLabel(l => `${l.source.name} - ${l.name} - ${l.target.name}`) - .linkCanvasObject((link, ctx) => this.paintLink(link, ctx)) - .linkCanvasObjectMode(() => "after") - .linkDirectionalArrowLength(4) - .linkDirectionalArrowRelPos(1) - .linkWidth(2) - .linkColor(() => this.css.mutedTextColor) - .d3VelocityDecay(0.2) - .onNodeClick(node => this.nodeClicked(node)); - - this.graph.d3Force('link').distance(50); - - this.graph.d3Force('center').strength(0.9); - - this.graph.d3Force('charge').strength(-30); - this.graph.d3Force('charge').distanceMax(400); - - this.renderData(await this.loadNotesAndRelations(this.noteId,2)); - } - - renderData(data, zoomToFit = true, zoomPadding = 10) { - this.graph.graphData(data); - - if (zoomToFit && data.nodes.length > 1) { - setTimeout(() => this.graph.zoomToFit(400, zoomPadding), 1000); - } - } - - async nodeClicked(node) { - if (!node.expanded) { - this.renderData( - await this.loadNotesAndRelations(node.id,1), - false - ); - } - else { - await appContext.tabManager.getActiveContext().setNote(node.id); - } - } - - async loadNotesAndRelations(noteId, maxDepth) { - const resp = await server.post(`notes/${noteId}/link-map`, { - maxNotes: 1000, - maxDepth - }); - - this.noteIdToLinkCountMap = {...this.noteIdToLinkCountMap, ...resp.noteIdToLinkCountMap}; - - 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 = {}; - - for (const link of Object.values(this.linkIdToLinkMap)) { - noteIdToLinkIdMap[link.sourceNoteId] = noteIdToLinkIdMap[link.sourceNoteId] || new Set(); - noteIdToLinkIdMap[link.sourceNoteId].add(link.id); - - noteIdToLinkIdMap[link.targetNoteId] = noteIdToLinkIdMap[link.targetNoteId] || new Set(); - noteIdToLinkIdMap[link.targetNoteId].add(link.id); - - const key = `${link.sourceNoteId}-${link.targetNoteId}`; - - if (key in linksGroupedBySourceTarget) { - if (!linksGroupedBySourceTarget[key].names.includes(link.name)) { - linksGroupedBySourceTarget[key].names.push(link.name); - } - } - else { - linksGroupedBySourceTarget[key] = { - id: key, - sourceNoteId: link.sourceNoteId, - targetNoteId: link.targetNoteId, - names: [link.name] - } - } - } - - return { - nodes: notes.map(note => ({ - id: note.noteId, - name: note.title, - type: note.type, - expanded: this.noteIdToLinkCountMap[note.noteId] === noteIdToLinkIdMap[note.noteId].size - })), - links: Object.values(linksGroupedBySourceTarget).map(link => ({ - id: link.id, - source: link.sourceNoteId, - target: link.targetNoteId, - name: link.names.join(", ") - })) - }; - } - - paintLink(link, ctx) { - if (this.zoomLevel < 5) { - return; - } - - ctx.font = '3px ' + this.css.fontFamily; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = this.css.mutedTextColor; - - const {source, target} = link; - - const x = (source.x + target.x) / 2; - const y = (source.y + target.y) / 2; - - ctx.save(); - ctx.translate(x, y); - - const deltaY = source.y - target.y; - const deltaX = source.x - target.x; - - let angle = Math.atan2(deltaY, deltaX); - let moveY = 2; - - if (angle < -Math.PI / 2 || angle > Math.PI / 2) { - angle += Math.PI; - moveY = -2; - } - - ctx.rotate(angle); - ctx.fillText(link.name, 0, moveY); - ctx.restore(); - } - - paintNode(node, color, ctx) { - const {x, y} = node; - - 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.fill(); - - if (this.zoomLevel < 2) { - return; - } - - if (!node.expanded) { - ctx.fillStyle = this.css.textColor; - ctx.font = 10 + 'px ' + this.css.fontFamily; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillText("+", x, y + 0.5); - } - - ctx.fillStyle = this.css.textColor; - ctx.font = 5 + 'px ' + this.css.fontFamily; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - - let title = node.name; - - if (title.length > 15) { - title = title.substr(0, 15) + "..."; - } - - ctx.fillText(title, x, y + (node.id === this.noteId ? 11 : 7)); - } - - stringToColor(str) { - let hash = 0; - for (let i = 0; i < str.length; i++) { - hash = str.charCodeAt(i) + ((hash << 5) - hash); - } - let colour = '#'; - for (let i = 0; i < 3; i++) { - const value = (hash >> (i * 8)) & 0xFF; - colour += ('00' + value.toString(16)).substr(-2); - } - return colour; - } - - rgb2hex(rgb) { - return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) - .slice(1) - .map(n => parseInt(n, 10).toString(16).padStart(2, '0')) - .join('')}` - } - - entitiesReloadedEvent({loadResults}) { - if (loadResults.getAttributes().find(attr => attr.type === 'relation' && (attr.noteId === this.noteId || attr.value === this.noteId))) { - this.refresh(); - } - } } diff --git a/src/public/app/widgets/type_widgets/note_map.js b/src/public/app/widgets/type_widgets/note_map.js index 9ff0dad96..5e0680b9c 100644 --- a/src/public/app/widgets/type_widgets/note_map.js +++ b/src/public/app/widgets/type_widgets/note_map.js @@ -9,7 +9,7 @@ export default class NoteMapTypeWidget extends TypeWidget { constructor() { super(); - this.noteMapWidget = new NoteMapWidget(); + this.noteMapWidget = new NoteMapWidget('type'); this.child(this.noteMapWidget); } @@ -21,8 +21,6 @@ export default class NoteMapTypeWidget extends TypeWidget { } async doRefresh(note) { - console.log("isEnabled", this.noteMapWidget.isEnabled()); - await this.noteMapWidget.refresh(); } }