@@ -12,17 +12,17 @@ import { BytesToSizePipe } from "../../../../shared/pipes/bytes-to-size/bytes-to
1212import { SvgIconComponent } from 'angular-svg-icon' ;
1313import { LoadingComponent } from "../../../../shared/components/loading/loading.component" ;
1414import { ImageMetadata } from "../../../../models/image-metadata" ;
15- import { ReplacePipe } from "../../../../shared/pipes/replace/replace.pipe" ;
1615import { TermStanza } from "../../../../obo/TermStanza" ;
1716import { DockerfileService } from "../../../../services/dockerfile.service" ;
1817import { DropdownComponent } from "../../../../shared/components/dropdown/dropdown.component" ;
18+ import { RelatedSoftware } from "../../../../models/related-software" ;
1919
2020@Component ( {
2121 selector : 'app-container' ,
2222 templateUrl : './container.component.html' ,
2323 styleUrl : './container.component.css' ,
2424 host : { '[class.dark]' : 'isDarkTheme()' } ,
25- imports : [ DatePipe , SlicePipe , MarkdownModule , TabsComponent , BytesToSizePipe , ClipboardButtonComponent , SvgIconComponent , LoadingComponent , NgTemplateOutlet , ReplacePipe , RouterLink , DropdownComponent ]
25+ imports : [ DatePipe , SlicePipe , MarkdownModule , TabsComponent , BytesToSizePipe , ClipboardButtonComponent , SvgIconComponent , LoadingComponent , NgTemplateOutlet , RouterLink , DropdownComponent ]
2626} )
2727export class ContainerComponent {
2828 /* Services */
@@ -41,6 +41,7 @@ export class ContainerComponent {
4141 containerTags : Signal < DockerHubTag [ ] > = signal ( [ ] ) ;
4242 ontologyCategories : Signal < TermStanza [ ] [ ] > = signal ( [ ] ) ;
4343 dockerfileContent : Signal < string | undefined > = signal < string > ( '' ) ;
44+ relatedSoftware : Signal < RelatedSoftware | null > = signal ( null ) ;
4445
4546 selectedTab = signal < TabName > ( TabName . README ) ;
4647
@@ -89,6 +90,7 @@ export class ContainerComponent {
8990 this . containerTags = this . containerService . getContainerTagsRes ( containerName ) . value ;
9091 this . ontologyCategories = this . containerService . getContainerCategoryHierarchy ( containerName ) ;
9192 this . dockerfileContent = this . dockerfileService . getContainerDockerfileContent ( containerName ) . value ;
93+ this . relatedSoftware = this . containerService . getRelatedSoftwareRes ( ) . value ;
9294 } ) ;
9395 } ) ;
9496 this . viewportScroller . setOffset ( [ 0 , 150 ] ) ;
@@ -237,6 +239,81 @@ export class ContainerComponent {
237239 return parentIds . flat ( ) . concat ( category . id ) ;
238240 }
239241
242+ /**
243+ * Get sorted related software by cooccurrence count and name
244+ */
245+ getSortedRelatedSoftware ( ) : { name : string ; displayName : string ; count : number ; inBdip : boolean } [ ] {
246+ const relatedSoftwareData = this . relatedSoftware ( ) ;
247+ if ( ! relatedSoftwareData || ! this . container ) {
248+ return [ ] ;
249+ }
250+
251+ const currentSoftware = this . container . name . toLowerCase ( ) ;
252+ const currentEntry = relatedSoftwareData . software [ currentSoftware ] ;
253+
254+ if ( ! currentEntry || ! currentEntry . cooccurrences ) {
255+ return [ ] ;
256+ }
257+
258+ const allContainersMetadata = this . containerService . getAllContainersMetadataRes ( ) . value ( ) ;
259+ const bdipContainers = new Set ( allContainersMetadata ? Array . from ( allContainersMetadata . keys ( ) ) . map ( k => k . toLowerCase ( ) ) : [ ] ) ;
260+
261+ return Object . entries ( currentEntry . cooccurrences )
262+ . map ( ( [ softwareName , cooccurrence ] ) => {
263+ const softwareEntry = relatedSoftwareData . software [ softwareName ] ;
264+ const displayName = softwareEntry ?. names ?. [ 0 ] || softwareName ;
265+ const count = cooccurrence ?. count ?? 0 ;
266+ const inBdip = bdipContainers . has ( softwareName . toLowerCase ( ) ) ;
267+
268+ return {
269+ name : softwareName ,
270+ displayName,
271+ count,
272+ inBdip
273+ } ;
274+ } )
275+ . sort ( ( a , b ) => {
276+ // Sort by count desc
277+ if ( b . count !== a . count ) {
278+ return b . count - a . count ;
279+ }
280+ // Then by displayName asc (case-insensitive)
281+ return a . displayName . localeCompare ( b . displayName , undefined , { sensitivity : 'base' } ) ;
282+ } ) ;
283+ }
284+
285+ /**
286+ * Get articles where a specific software appears with the current container
287+ */
288+ getCooccurringArticles ( softwareName : string ) : string [ ] {
289+ const relatedSoftwareData = this . relatedSoftware ( ) ;
290+ if ( ! relatedSoftwareData || ! this . container ) {
291+ return [ ] ;
292+ }
293+
294+ const currentSoftware = this . container . name . toLowerCase ( ) ;
295+ const currentEntry = relatedSoftwareData . software [ currentSoftware ] ;
296+
297+ if ( ! currentEntry || ! currentEntry . cooccurrences ) {
298+ return [ ] ;
299+ }
300+
301+ const cooccurrence = currentEntry . cooccurrences [ softwareName ] ;
302+
303+ if ( ! cooccurrence || ! Array . isArray ( cooccurrence . articles ) ) {
304+ return [ ] ;
305+ }
306+
307+ return cooccurrence . articles ;
308+ }
309+
310+ /**
311+ * Generate Bio-Protocol article URL from article ID
312+ */
313+ getBioProtocolArticleUrl ( articleId : string ) : string {
314+ return `https://bio-protocol.org/en/bpdetail?id=${ articleId } &type=0` ;
315+ }
316+
240317 /* Markdown reactive files */
241318 /* ---------------------------------------------------------------------------------------------------------------- */
242319 getCliMarkdown ( containerMetadata : ImageMetadata ) : string {
@@ -375,4 +452,5 @@ enum TabName {
375452 TAGS = 'tags' ,
376453 TESTING = 'testing' ,
377454 DOCKERFILE = 'dockerfile' ,
455+ RELATED_SOFTWARE = 'related-software' ,
378456}
0 commit comments