11
2- import { CustomLink , LinkStatus } from 'types' ;
2+ import { ObsidianRenderer , ObsidianLink , LinkPair , GltLink , DataviewLinkType } from 'types' ;
3+ import { Page , getAPI } from 'obsidian-dataview' ;
4+ import { Text , TextStyle } from 'pixi.js' ;
5+ import extractLinks from 'markdown-link-extractor' ;
6+
37
48export class LinkManager {
5- linksMap : Map < string , CustomLink > ;
6- linkStatus : Map < string , LinkStatus . First | LinkStatus . Second > ;
9+ linksMap : Map < string , GltLink > ;
10+ api = getAPI ( ) ;
711
812 constructor ( ) {
9- this . linksMap = new Map < string , CustomLink > ( ) ;
10- this . linkStatus = new Map < string , LinkStatus . First | LinkStatus . Second > ( ) ;
13+ this . linksMap = new Map < string , GltLink > ( ) ;
1114 }
1215
1316 generateKey ( sourceId : string , targetId : string ) : string {
1417 return `${ sourceId } -${ targetId } ` ;
1518 }
1619
17- addLink ( link : CustomLink ) : void {
18- const key = this . generateKey ( link . source . id , link . target . id ) ;
19- const reverseKey = this . generateKey ( link . target . id , link . source . id ) ;
20+ addLink ( renderer : ObsidianRenderer , obLink : ObsidianLink ) : void {
21+ const key = this . generateKey ( obLink . source . id , obLink . target . id ) ;
22+ const reverseKey = this . generateKey ( obLink . target . id , obLink . source . id ) ;
23+ const pairStatus = ( obLink . source . id !== obLink . target . id ) && this . linksMap . has ( reverseKey ) ? LinkPair . Second : LinkPair . None ;
24+ const newLink : GltLink = {
25+ obsidianLink : obLink ,
26+ pairStatus : pairStatus ,
27+ pixiText : this . createTextForLink ( renderer , obLink , pairStatus )
28+ } ;
2029
21- // Add the new link
22- this . linksMap . set ( key , link ) ;
30+ this . linksMap . set ( key , newLink ) ;
2331
24- // Manage the pair statuses
25- if ( this . linksMap . has ( reverseKey ) ) {
26- // If the reverse link is already present, set the statuses accordingly
27- this . linkStatus . set ( key , LinkStatus . Second ) ;
28- this . linkStatus . set ( reverseKey , LinkStatus . First ) ;
29- } else {
30- // If it's a standalone link (no pair yet), do not assign a pair status
31- // This will be managed when the reverse link is added (if it happens)
32+ if ( ( obLink . source . id !== obLink . target . id ) && this . linksMap . has ( reverseKey ) ) {
33+ const reverseLink = this . linksMap . get ( reverseKey ) ;
34+ if ( reverseLink ) {
35+ reverseLink . pairStatus = LinkPair . First ;
36+ }
37+ console . log ( "New Pair: " , newLink . obsidianLink , reverseLink ?. obsidianLink ) ;
3238 }
3339 }
3440
35- removeLink ( link : CustomLink ) : void {
41+ removeLink ( renderer : ObsidianRenderer , link : ObsidianLink ) : void {
3642 const key = this . generateKey ( link . source . id , link . target . id ) ;
3743 const reverseKey = this . generateKey ( link . target . id , link . source . id ) ;
3844
39- this . linksMap . delete ( key ) ;
40- if ( this . linkStatus . has ( key ) ) {
41- this . linkStatus . delete ( key ) ;
45+ const gltLink = this . linksMap . get ( key ) ;
46+
47+ if ( gltLink && gltLink . pixiText && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiText ) ) {
48+ renderer . px . stage . removeChild ( gltLink . pixiText ) ;
49+ gltLink . pixiText . destroy ( ) ;
4250 }
43- if ( this . linkStatus . get ( reverseKey ) === LinkStatus . Second ) {
44- this . linkStatus . delete ( reverseKey ) ;
51+
52+ this . linksMap . delete ( key ) ;
53+
54+ const reverseLink = this . linksMap . get ( reverseKey ) ;
55+ if ( reverseLink && reverseLink . pairStatus !== LinkPair . None ) {
56+ reverseLink . pairStatus = LinkPair . None ;
4557 }
4658 }
4759
48- updateLinks ( currentLinks : CustomLink [ ] ) : void {
60+ removeLinks ( renderer : ObsidianRenderer , currentLinks : ObsidianLink [ ] ) : void {
4961 const currentKeys = new Set ( currentLinks . map ( link => this . generateKey ( link . source . id , link . target . id ) ) ) ;
50- for ( const key of this . linksMap . keys ( ) ) {
62+ // remove any links in our map that aren't in this list
63+ this . linksMap . forEach ( ( _ , key ) => {
5164 if ( ! currentKeys . has ( key ) ) {
5265 const link = this . linksMap . get ( key ) ;
5366 if ( link ) {
54- this . removeLink ( link ) ;
67+ this . removeLink ( renderer , link . obsidianLink ) ;
5568 }
5669 }
70+ } ) ;
71+ }
72+
73+ getLinkPairStatus ( key : string ) : LinkPair {
74+ const link = this . linksMap . get ( key ) ;
75+ return link ? link . pairStatus : LinkPair . None ;
76+ }
77+
78+ // Update the position of the text on the graph
79+ updateTextPosition ( renderer : ObsidianRenderer , link : ObsidianLink ) : void {
80+ if ( ! renderer || ! link || ! link . source || ! link . target ) {
81+ // If any of these are null, exit the function
82+ return ;
83+ }
84+ const linkKey = this . generateKey ( link . source . id , link . target . id ) ;
85+ const obsLink = this . linksMap . get ( linkKey ) ;
86+ let text ;
87+ if ( obsLink ) {
88+ text = obsLink . pixiText ;
89+ } else {
90+ return
91+ } ;
92+
93+ // Calculate the mid-point of the link
94+ const midX : number = ( link . source . x + link . target . x ) / 2 ;
95+ const midY : number = ( link . source . y + link . target . y ) / 2 ;
96+
97+ // Transform the mid-point coordinates based on the renderer's pan and scale
98+ const { x, y } = this . getLinkToTextCoordinates ( midX , midY , renderer . panX , renderer . panY , renderer . scale ) ;
99+ if ( text && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( text ) ) {
100+ // Set the position and scale of the text
101+ text . x = x ;
102+ text . y = y ;
103+ text . scale . set ( 1 / ( 3 * renderer . nodeScale ) ) ;
104+ }
105+ }
106+
107+ // Create or update text for a given link
108+ private createTextForLink ( renderer : ObsidianRenderer , link : ObsidianLink , pairStatus : LinkPair ) : Text | null {
109+
110+ // Get the text to display for the link
111+ let linkString : string | null = this . getMetadataKeyForLink ( link . source . id , link . target . id ) ;
112+ if ( linkString === null ) {
113+ return null ;
114+ } //doesn't add if link is null
115+ if ( link . source . id === link . target . id ) {
116+ linkString = "" ;
117+ }
118+
119+ if ( pairStatus === LinkPair . None ) {
120+
121+ } else if ( pairStatus === LinkPair . First ) {
122+ linkString = linkString + "\n\n" ;
123+ } else if ( pairStatus === LinkPair . Second ) {
124+ linkString = "\n\n" + linkString ;
125+ } else {
126+
127+ }
128+ // Define the style for the text
129+ const textStyle : TextStyle = new TextStyle ( {
130+ fontFamily : 'Arial' ,
131+ fontSize : 36 ,
132+ fill : this . determineTextColor ( )
133+ } ) ;
134+ // Create new text node
135+ const text : Text = new Text ( linkString , textStyle ) ;
136+ text . alpha = 0.7 ;
137+ text . anchor . set ( 0.5 , 0.5 ) ;
138+
139+
140+ this . updateTextPosition ( renderer , link ) ;
141+ renderer . px . stage . addChild ( text ) ;
142+ return text
143+ }
144+
145+ // Utility function to extract the file path from a Markdown link
146+ private extractPathFromMarkdownLink ( markdownLink : string | unknown ) : string {
147+ const links = extractLinks ( markdownLink ) . links ;
148+ // The package returns an array of links. Assuming you want the first link.
149+ return links . length > 0 ? links [ 0 ] : '' ;
150+ }
151+
152+ private determineTextColor ( ) : string {
153+ // Get the computed style of the document body
154+ const style = getComputedStyle ( document . body ) ;
155+
156+ // This is a basic check. You might need to adjust the logic based on the themes you support.
157+ // Here, we assume that dark themes have a background color with a low brightness value.
158+ let textColor = '#FF0000' ;
159+ if ( style && style . backgroundColor && style . backgroundColor ) {
160+ const isDarkTheme = style . backgroundColor . match ( / \d + / g) ?. map ( Number ) . slice ( 0 , 3 ) . reduce ( ( a , b ) => a + b , 0 ) < 382.5 ;
161+ isDarkTheme ? textColor = '#FFFFFF' : textColor = '#000000' ; // White text for dark themes, black for light themes)
162+ }
163+
164+ return textColor
165+ }
166+
167+ // Method to determine the type of a value, now a class method
168+ private determineDataviewLinkType ( value : any ) : DataviewLinkType {
169+ if ( typeof value === 'object' && value !== null && 'path' in value ) {
170+ return DataviewLinkType . WikiLink ;
171+ } else if ( typeof value === 'string' && value . includes ( '](' ) ) {
172+ return DataviewLinkType . MarkdownLink ;
173+ } else if ( typeof value === 'string' ) {
174+ return DataviewLinkType . String ;
175+ } else if ( Array . isArray ( value ) ) {
176+ return DataviewLinkType . Array ;
177+ } else {
178+ return DataviewLinkType . Other ;
179+ }
180+ }
181+
182+ // Remove all text nodes from the graph
183+ destroyMap ( renderer : ObsidianRenderer ) : void {
184+ if ( this . linksMap . size > 0 ) {
185+ this . linksMap . forEach ( ( gltLink , linkKey ) => {
186+ if ( gltLink . pixiText && renderer . px && renderer . px . stage && renderer . px . stage . children && renderer . px . stage . children . includes ( gltLink . pixiText ) ) {
187+ renderer . px . stage . removeChild ( gltLink . pixiText ) ;
188+ gltLink . pixiText . destroy ( ) ;
189+ }
190+ this . linksMap . delete ( linkKey ) ;
191+ } ) ;
57192 }
58193 }
59194
60- getLinkStatus ( key : string ) : LinkStatus {
61- const status = this . linkStatus . get ( key )
62- if ( status !== undefined ) {
63- return status
64- } else {
65- return LinkStatus . None
195+ // Get the metadata key for a link between two pages
196+ private getMetadataKeyForLink ( sourceId : string , targetId : string ) : string | null {
197+ const sourcePage : Page | undefined = this . api . page ( sourceId ) ;
198+ if ( ! sourcePage ) return null ;
199+
200+ for ( const [ key , value ] of Object . entries ( sourcePage ) ) {
201+ const valueType = this . determineDataviewLinkType ( value ) ;
202+
203+ switch ( valueType ) {
204+ case DataviewLinkType . WikiLink :
205+ if ( value . path === targetId ) {
206+ return key ;
207+ }
208+ break ;
209+ case DataviewLinkType . MarkdownLink :
210+ if ( this . extractPathFromMarkdownLink ( value ) === targetId ) {
211+ return key ;
212+ }
213+ break ;
214+ case DataviewLinkType . Array :
215+ for ( const item of value ) {
216+ if ( this . determineDataviewLinkType ( item ) === DataviewLinkType . WikiLink && item . path === targetId ) {
217+ return key ;
218+ }
219+ if ( this . determineDataviewLinkType ( item ) === DataviewLinkType . MarkdownLink && this . extractPathFromMarkdownLink ( item ) === targetId ) {
220+ return key ;
221+ }
222+ }
223+ break ;
224+ // Handle other cases as needed
225+ }
66226 }
227+ return null ;
228+ }
229+
230+ // Function to calculate the coordinates for placing the link text.
231+ private getLinkToTextCoordinates ( linkX : number , linkY : number , panX : number , panY : number , scale : number ) : { x : number , y : number } {
232+ // Apply scaling and panning to calculate the actual position.
233+ return { x : linkX * scale + panX , y : linkY * scale + panY } ;
67234 }
235+
236+
68237}
0 commit comments