Skip to content

Commit 73f5d96

Browse files
authored
GDC WSI support. (#2644)
1 parent 7ae0978 commit 73f5d96

File tree

21 files changed

+1501
-281
lines changed

21 files changed

+1501
-281
lines changed

client/plots/wsiviewer/WSIViewer.ts

Lines changed: 65 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getCompInit } from '#rx'
22
import 'ol/ol.css'
3-
import Map from 'ol/Map.js'
3+
import OLMap from 'ol/Map.js'
44
import TileLayer from 'ol/layer/Tile.js'
55
import Tile from 'ol/layer/Tile.js'
66
import View from 'ol/View.js'
@@ -15,7 +15,6 @@ import wsiViewerDefaults from '#plots/wsiviewer/defaults.ts'
1515
import type { WSImagesRequest, WSImagesResponse, WSImage } from '#types'
1616
import wsiViewerImageFiles from './wsimagesloaded.ts'
1717
import { table2col } from '#dom/table2col'
18-
1918
export default class WSIViewer {
2019
// following attributes are required by rx
2120
private type: string
@@ -27,10 +26,16 @@ export default class WSIViewer {
2726

2827
private thumbnailsContainer: any
2928

29+
// Field to store sample ID to wsi session ID mapping
30+
private readonly wsiSessions: Map<string, string | undefined>
31+
3032
constructor(opts: any) {
3133
this.type = 'WSIViewer'
3234
this.opts = opts
3335
this.wsiViewerInteractions = new WSIViewerInteractions(this, opts)
36+
this.wsiSessions = new Map()
37+
// Add event listener for tab/window close
38+
window.addEventListener('beforeunload', this.onTabClosed.bind(this))
3439
}
3540

3641
async main(): Promise<void> {
@@ -88,6 +93,61 @@ export default class WSIViewer {
8893
this.renderMetadata(holder, layers, settings)
8994
}
9095

96+
private async getLayers(state: any, wsimages: WSImage[]): Promise<Array<TileLayer<Zoomify>>> {
97+
const layers: Array<TileLayer<Zoomify>> = []
98+
99+
for (let i = 0; i < wsimages.length; i++) {
100+
const body: WSImagesRequest = {
101+
genome: state.genome || state.vocab.genome,
102+
dslabel: state.dslabel || state.vocab.dslabel,
103+
sampleId: state.sample_id,
104+
wsimage: wsimages[i].filename
105+
}
106+
107+
const data: WSImagesResponse = await dofetch3('wsimages', { body })
108+
109+
if (data.status === 'error') {
110+
return []
111+
}
112+
113+
this.wsiSessions.set(wsimages[i].filename, data.browserImageInstanceId)
114+
115+
const imgWidth = data.slide_dimensions[0]
116+
const imgHeight = data.slide_dimensions[1]
117+
118+
const zoomifyUrl = `/tileserver/layer/slide/${data.wsiSessionId}/zoomify/{TileGroup}/{z}-{x}-{y}@1x.jpg`
119+
120+
const source = new Zoomify({
121+
url: zoomifyUrl,
122+
size: [imgWidth, imgHeight],
123+
crossOrigin: 'anonymous',
124+
zDirection: -1 // Ensure we get a tile with the screen resolution or higher
125+
})
126+
127+
const options = {
128+
preview: `/tileserver/layer/slide/${data.wsiSessionId}/zoomify/TileGroup0/0-0-0@1x.jpg`,
129+
metadata: wsimages[i].metadata,
130+
source: source,
131+
baseLayer: true
132+
}
133+
134+
const layer = new TileLayer(options)
135+
136+
layers.push(layer)
137+
}
138+
return layers
139+
}
140+
141+
private onTabClosed() {
142+
const hasNonUndefinedValue = Array.from(this.wsiSessions.values()).some(value => value !== undefined)
143+
if (hasNonUndefinedValue) {
144+
dofetch3('clearwsisession', {
145+
method: 'DELETE',
146+
body: { sessions: JSON.stringify(Array.from(this.wsiSessions)) }
147+
})
148+
}
149+
}
150+
91151
private generateThumbnails(layers: Array<TileLayer<Zoomify>>, setting: Settings) {
92152
if (!this.thumbnailsContainer) {
93153
// First-time initialization
@@ -138,8 +198,8 @@ export default class WSIViewer {
138198
}
139199
}
140200

141-
private getMap(displayedImage: TileLayer) {
142-
return new Map({
201+
private getMap(displayedImage: TileLayer): OLMap {
202+
return new OLMap({
143203
layers: [displayedImage],
144204
target: 'wsi-viewer',
145205
view: new View({
@@ -148,7 +208,7 @@ export default class WSIViewer {
148208
})
149209
}
150210

151-
private addControls(map: Map, firstLayer: TileLayer) {
211+
private addControls(map: OLMap, firstLayer: TileLayer) {
152212
const fullscreen = new FullScreen()
153213
map.addControl(fullscreen)
154214

@@ -164,50 +224,6 @@ export default class WSIViewer {
164224
map.addControl(overviewMapControl)
165225
}
166226

167-
private async getLayers(state: any, wsimages: WSImage[]): Promise<Array<TileLayer<Zoomify>>> {
168-
const layers: Array<TileLayer<Zoomify>> = []
169-
170-
for (let i = 0; i < wsimages.length; i++) {
171-
const body: WSImagesRequest = {
172-
genome: state.genome || state.vocab.genome,
173-
dslabel: state.dslabel || state.vocab.dslabel,
174-
sampleId: state.sample_id,
175-
wsimage: wsimages[i].filename
176-
}
177-
178-
const data: WSImagesResponse = await dofetch3('wsimages', { body })
179-
180-
if (data.status === 'error') {
181-
return []
182-
}
183-
184-
const imgWidth = data.slide_dimensions[0]
185-
const imgHeight = data.slide_dimensions[1]
186-
187-
const zoomifyUrl = `/tileserver/layer/slide/${data.sessionId}/zoomify/{TileGroup}/{z}-{x}-{y}@1x.jpg`
188-
189-
const source = new Zoomify({
190-
url: zoomifyUrl,
191-
size: [imgWidth, imgHeight],
192-
crossOrigin: 'anonymous',
193-
zDirection: -1 // Ensure we get a tile with the screen resolution or higher
194-
})
195-
196-
const options = {
197-
// title: "Set Title",
198-
preview: `/tileserver/layer/slide/${data.sessionId}/zoomify/TileGroup0/0-0-0@1x.jpg`,
199-
metadata: wsimages[i].metadata,
200-
source: source,
201-
baseLayer: true
202-
}
203-
204-
const layer = new TileLayer(options)
205-
206-
layers.push(layer)
207-
}
208-
return layers
209-
}
210-
211227
private renderMetadata(holder: any, layers: Array<TileLayer<Zoomify>>, settings: Settings) {
212228
holder.select('div[id="metadata"]').remove()
213229
const holderDiv = holder.append('div').attr('id', 'metadata')

client/plots/wsiviewer/plot.wsi.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export default async function (dslabel, holder, genomeObj, sample_id) {
1818
try {
1919
const opts = {
2020
holder: holder,
21-
2221
state: {
2322
genome: genomeObj.name,
2423
dslabel: dslabel,

client/plots/wsiviewer/wsimagesloaded.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { dofetch3 } from '#common/dofetch'
33
export default async function wsiViewerImageFiles(o) {
44
const data = await dofetch3('samplewsimages', {
55
body: {
6-
genome: o.app.opts.state.vocab.genome,
7-
dslabel: o.app.opts.state.vocab.dslabel,
6+
genome: o.app.opts.state.genome,
7+
dslabel: o.app.opts.state.dslabel,
88
sample_id: o.app.opts.state.sample_id
99
}
1010
})

client/src/app.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -566,6 +566,10 @@ async function parseEmbedThenUrl(arg, app) {
566566
return await launchDisco(arg, app)
567567
}
568568

569+
if (arg.wsiViewer) {
570+
return await launchWsiViewer(arg, app)
571+
}
572+
569573
if (arg.geneSearch4GDCmds3) {
570574
/* can generalize by changing to geneSearch4tk:{tkobj}
571575
so it's no longer hardcoded for one dataset of one track type
@@ -1527,3 +1531,19 @@ async function launchDisco(arg, app) {
15271531
return await _.launch(arg.disco, genomeObj, app.holder0)
15281532
}
15291533
}
1534+
1535+
async function launchWsiViewer(arg, app) {
1536+
if (!arg.genome) throw '"genome" parameter missing'
1537+
const genomeObj = app.genomes[arg.genome]
1538+
if (!genomeObj) throw 'unknown genome'
1539+
const wsiViewer = await import('#plots/wsiviewer/plot.wsi.js')
1540+
1541+
// Extract sample-id from the URL
1542+
const urlParams = new URLSearchParams(window.location.search)
1543+
const sampleId = urlParams.get('sample_id')
1544+
if (!sampleId) {
1545+
throw 'sample-id parameter missing'
1546+
}
1547+
1548+
wsiViewer.default(arg.dslabel, app.holder, genomeObj, sampleId)
1549+
}

0 commit comments

Comments
 (0)