@@ -27,7 +27,7 @@ import { default as MapEventType } from "ol/MapEventType";
2727import { getCenter } from 'ol/extent' ;
2828import { toStringXY } from 'ol/coordinate' ;
2929
30- import { formatImageMetadata } from './metadata.js' ;
30+ import { formatImageMetadata , getFrameMapping } from './metadata.js' ;
3131import { ROI } from './roi.js' ;
3232import { generateUID } from './utils.js' ;
3333import {
@@ -42,8 +42,14 @@ import {
4242import DICOMwebClient from 'dicomweb-client/build/dicomweb-client.js'
4343
4444
45+ function _getPixelSpacing ( metadata ) {
46+ const functionalGroup = metadata . SharedFunctionalGroupsSequence [ 0 ] ;
47+ const pixelMeasures = functionalGroup . PixelMeasuresSequence [ 0 ] ;
48+ return pixelMeasures . PixelSpacing ;
49+ }
50+
4551function _geometry2Scoord3d ( geometry , pyramid ) {
46- const frameOfReferenceUID = pyramid [ pyramid . length - 1 ] . frameOfReferenceUID ;
52+ const frameOfReferenceUID = pyramid [ pyramid . length - 1 ] . FrameOfReferenceUID ;
4753 const type = geometry . getType ( ) ;
4854 if ( type === 'Point' ) {
4955 let coordinates = geometry . getCoordinates ( ) ;
@@ -161,9 +167,10 @@ function _coordinateFormatGeometry2Scoord3d(coordinates, pyramid) {
161167 coordinates = [ coordinates ] ;
162168 }
163169 coordinates . map ( coord => {
164- let x = ( coord [ 0 ] * pyramid [ pyramid . length - 1 ] . pixelSpacing [ 0 ] ) . toFixed ( 4 ) ;
165- let y = ( - ( coord [ 1 ] - 1 ) * pyramid [ pyramid . length - 1 ] . pixelSpacing [ 1 ] ) . toFixed ( 4 ) ;
166- let z = ( 1 ) . toFixed ( 4 ) ;
170+ const pixelSpacing = _getPixelSpacing ( pyramid [ pyramid . length - 1 ] ) ;
171+ const x = ( coord [ 0 ] * pixelSpacing [ 0 ] ) . toFixed ( 4 ) ;
172+ const y = ( - ( coord [ 1 ] - 1 ) * pixelSpacing [ 1 ] ) . toFixed ( 4 ) ;
173+ const z = ( 1 ) . toFixed ( 4 ) ;
167174 coordinates = [ Number ( x ) , Number ( y ) , Number ( z ) ] ;
168175 } )
169176 return ( coordinates ) ;
@@ -178,9 +185,10 @@ function _coordinateFormatScoord3d2Geometry(coordinates, pyramid) {
178185 coordinates = [ coordinates ] ;
179186 }
180187 coordinates . map ( coord => {
181- let x = ( coord [ 0 ] / pyramid [ pyramid . length - 1 ] . pixelSpacing [ 0 ] - 1 ) ;
182- let y = - ( coord [ 1 ] / pyramid [ pyramid . length - 1 ] . pixelSpacing [ 1 ] - 1 ) ;
183- let z = coord [ 2 ] ;
188+ const pixelSpacing = _getPixelSpacing ( pyramid [ pyramid . length - 1 ] ) ;
189+ const x = ( coord [ 0 ] / pixelSpacing [ 0 ] - 1 ) ;
190+ const y = - ( coord [ 1 ] / pixelSpacing [ 1 ] - 1 ) ;
191+ const z = coord [ 2 ] ;
184192 coordinates = [ x , y , z ] ;
185193 } ) ;
186194 return ( coordinates ) ;
@@ -207,11 +215,12 @@ const _features = Symbol('features');
207215const _drawingSource = Symbol ( 'drawingSource' ) ;
208216const _drawingLayer = Symbol ( 'drawingLayer' ) ;
209217const _segmentations = Symbol ( 'segmentations' ) ;
210- const _pyramid = Symbol ( 'pyramid' ) ;
211218const _client = Symbol ( 'client' ) ;
212219const _controls = Symbol ( 'controls' ) ;
213220const _interactions = Symbol ( 'interactions' ) ;
214- const _pyramidBase = Symbol ( 'pyramidBaseLayer' ) ;
221+ const _pyramidMetadata = Symbol ( 'pyramidMetadata' ) ;
222+ const _pyramidFrameMappings = Symbol ( 'pyramidFrameMappings' ) ;
223+ const _pyramidBaseMetadata = Symbol ( 'pyramidMetadataBase' ) ;
215224const _metadata = Symbol ( 'metadata' ) ;
216225
217226
@@ -262,44 +271,69 @@ class VLWholeSlideMicroscopyImageViewer {
262271 * images at the different pyramid levels.
263272 */
264273 this [ _metadata ] = options . metadata . map ( m => formatImageMetadata ( m ) ) ;
265- this . _pyramid = [ ] ;
274+ // Sort instances and optionally concatenation parts if present.
275+ this [ _metadata ] . sort ( ( a , b ) => {
276+ const sizeDiff = a . TotalPixelMatrixColumns - b . TotalPixelMatrixColumns ;
277+ if ( sizeDiff !== 0 ) {
278+ return sizeDiff ;
279+ }
280+ if ( a . ConcatenationFrameOffsetNumber !== undefined ) {
281+ return a . ConcatenationFrameOffsetNumber - b . ConcatenationFrameOffsetNumber ;
282+ }
283+ return sizeDiff ;
284+ } ) ;
285+ this [ _pyramidMetadata ] = [ ] ;
286+ this [ _pyramidFrameMappings ] = [ ] ;
287+ let frameMappings = options . metadata . map ( m => getFrameMapping ( m ) ) ;
266288 for ( let i = 0 ; i < this [ _metadata ] . length ; i ++ ) {
267- const cols = this [ _metadata ] [ i ] . totalPixelMatrixColumns ;
268- const rows = this [ _metadata ] [ i ] . totalPixelMatrixRows ;
269- const mapping = this [ _metadata ] [ i ] . frameMapping ;
289+ const cols = this [ _metadata ] [ i ] . TotalPixelMatrixColumns ;
290+ const rows = this [ _metadata ] [ i ] . TotalPixelMatrixRows ;
291+ const numberOfFrames = this [ _metadata ] [ i ] . NumberOfFrames ;
292+ const perFrameFunctionalGroups = this [ _metadata ] [ i ] . PerFrameFunctionalGroupsSequence ;
270293 /*
271294 * Instances may be broken down into multiple concatentation parts.
272295 * Therefore, we have to re-assemble instance metadata.
273296 */
274297 let alreadyExists = false ;
275298 let index = null ;
276- for ( let j = 0 ; j < this . _pyramid . length ; j ++ ) {
299+ for ( let j = 0 ; j < this [ _pyramidMetadata ] . length ; j ++ ) {
277300 if (
278- ( this . _pyramid [ j ] . totalPixelMatrixColumns === cols ) &&
279- ( this . _pyramid [ j ] . totalPixelMatrixRows === rows )
301+ ( this [ _pyramidMetadata ] [ j ] . TotalPixelMatrixColumns === cols ) &&
302+ ( this [ _pyramidMetadata ] [ j ] . TotalPixelMatrixRows === rows )
280303 ) {
281304 alreadyExists = true ;
282305 index = j ;
283306 }
284307 }
285308 if ( alreadyExists ) {
286309 // Update with information obtained from current concatentation part.
287- Object . assign ( this . _pyramid [ index ] . frameMapping , mapping ) ;
310+ Object . assign ( this [ _pyramidFrameMappings ] [ index ] , frameMappings [ i ] ) ;
311+ this [ _pyramidMetadata ] [ index ] . NumberOfFrames += numberOfFrames ;
312+ this [ _pyramidMetadata ] [ index ] . PerFrameFunctionalGroupsSequence . push (
313+ ...perFrameFunctionalGroups
314+ ) ;
315+ if ( ! "SOPInstanceUIDOfConcatenationSource" in this [ _metadata ] [ i ] ) {
316+ throw new Error (
317+ 'Attribute "SOPInstanceUIDOfConcatenationSource" is required ' +
318+ 'for concatenation parts.'
319+ ) ;
320+ }
321+ const sopInstanceUID = this [ _metadata ] [ i ] . SOPInstanceUIDOfConcatenationSource ;
322+ this [ _pyramidMetadata ] [ index ] . SOPInstanceUID = sopInstanceUID ;
323+ delete this [ _pyramidMetadata ] [ index ] . SOPInstanceUIDOfConcatenationSource ;
324+ delete this [ _pyramidMetadata ] [ index ] . ConcatenationUID ;
325+ delete this [ _pyramidMetadata ] [ index ] . InConcatenationNumber ;
326+ delete this [ _pyramidMetadata ] [ index ] . ConcatenationFrameOffsetNumber ;
288327 } else {
289- this . _pyramid . push ( this [ _metadata ] [ i ] ) ;
328+ this [ _pyramidMetadata ] . push ( this [ _metadata ] [ i ] ) ;
329+ this [ _pyramidFrameMappings ] . push ( frameMappings [ i ] ) ;
290330 }
291331 }
292- // Sort levels in ascending order
293- this . _pyramid . sort ( function ( a , b ) {
294- if ( a . totalPixelMatrixColumns < b . totalPixelMatrixColumns ) {
295- return - 1 ;
296- } else if ( a . totalPixelMatrixColumns > b . totalPixelMatrixColumns ) {
297- return 1 ;
298- } else {
299- return 0 ;
300- }
301- } ) ;
302- this [ _pyramidBase ] = this . _pyramid [ this . _pyramid . length - 1 ] ;
332+ const nLevels = this [ _pyramidMetadata ] . length ;
333+ if ( nLevels === 0 ) {
334+ console . error ( 'empty pyramid - no levels found' )
335+ }
336+ this [ _pyramidBaseMetadata ] = this [ _pyramidMetadata ] [ nLevels - 1 ] ;
303337 /*
304338 * Collect relevant information from DICOM metadata for each pyramid
305339 * level to construct the Openlayers map.
@@ -309,31 +343,33 @@ class VLWholeSlideMicroscopyImageViewer {
309343 const resolutions = [ ] ;
310344 const origins = [ ] ;
311345 const offset = [ 0 , - 1 ] ;
312- const nLevels = this . _pyramid . length ;
313- if ( nLevels === 0 ) {
314- console . error ( 'empty pyramid - no levels found' )
315- }
316- const basePixelSpacing = this . _pyramid [ nLevels - 1 ] . pixelSpacing ;
317- const baseColumns = this . _pyramid [ nLevels - 1 ] . columns ;
318- const baseRows = this . _pyramid [ nLevels - 1 ] . rows ;
319- const baseTotalPixelMatrixColumns = this . _pyramid [ nLevels - 1 ] . totalPixelMatrixColumns ;
320- const baseTotalPixelMatrixRows = this . _pyramid [ nLevels - 1 ] . totalPixelMatrixRows ;
346+ const basePixelSpacing = _getPixelSpacing ( this [ _pyramidBaseMetadata ] ) ;
347+ const baseColumns = this [ _pyramidBaseMetadata ] . Columns ;
348+ const baseRows = this [ _pyramidBaseMetadata ] . Rows ;
349+ const baseTotalPixelMatrixColumns = this [ _pyramidBaseMetadata ] . TotalPixelMatrixColumns ;
350+ const baseTotalPixelMatrixRows = this [ _pyramidBaseMetadata ] . TotalPixelMatrixRows ;
321351 const baseColFactor = Math . ceil ( baseTotalPixelMatrixColumns / baseColumns ) ;
322352 const baseRowFactor = Math . ceil ( baseTotalPixelMatrixRows / baseRows ) ;
323353 const baseAdjustedTotalPixelMatrixColumns = baseColumns * baseColFactor ;
324354 const baseAdjustedTotalPixelMatrixRows = baseRows * baseRowFactor ;
325355 for ( let j = ( nLevels - 1 ) ; j >= 0 ; j -- ) {
326- let columns = this . _pyramid [ j ] . columns ;
327- let rows = this . _pyramid [ j ] . rows ;
328- let totalPixelMatrixColumns = this . _pyramid [ j ] . totalPixelMatrixColumns ;
329- let totalPixelMatrixRows = this . _pyramid [ j ] . totalPixelMatrixRows ;
330- let pixelSpacing = this . _pyramid [ j ] . pixelSpacing ;
331- let colFactor = Math . ceil ( totalPixelMatrixColumns / columns ) ;
332- let rowFactor = Math . ceil ( totalPixelMatrixRows / rows ) ;
333- let adjustedTotalPixelMatrixColumns = columns * colFactor ;
334- let adjustedTotalPixelMatrixRows = rows * rowFactor ;
335- tileSizes . push ( [ columns , rows ] ) ;
336- totalSizes . push ( [ adjustedTotalPixelMatrixColumns , adjustedTotalPixelMatrixRows ] ) ;
356+ const columns = this [ _pyramidMetadata ] [ j ] . Columns ;
357+ const rows = this [ _pyramidMetadata ] [ j ] . Rows ;
358+ const totalPixelMatrixColumns = this [ _pyramidMetadata ] [ j ] . TotalPixelMatrixColumns ;
359+ const totalPixelMatrixRows = this [ _pyramidMetadata ] [ j ] . TotalPixelMatrixRows ;
360+ const pixelSpacing = _getPixelSpacing ( this [ _pyramidMetadata ] [ j ] ) ;
361+ const colFactor = Math . ceil ( totalPixelMatrixColumns / columns ) ;
362+ const rowFactor = Math . ceil ( totalPixelMatrixRows / rows ) ;
363+ const adjustedTotalPixelMatrixColumns = columns * colFactor ;
364+ const adjustedTotalPixelMatrixRows = rows * rowFactor ;
365+ tileSizes . push ( [
366+ columns ,
367+ rows
368+ ] ) ;
369+ totalSizes . push ( [
370+ adjustedTotalPixelMatrixColumns ,
371+ adjustedTotalPixelMatrixRows
372+ ] ) ;
337373
338374 /*
339375 * Compute the resolution at each pyramid level, since the zoom
@@ -353,13 +389,15 @@ class VLWholeSlideMicroscopyImageViewer {
353389 tileSizes . reverse ( ) ;
354390 origins . reverse ( ) ;
355391
356- const pyramid = this . _pyramid ;
392+ // Functions won't be able to access "this"
393+ const pyramid = this [ _pyramidMetadata ] ;
394+ const pyramidFrameMappings = this [ _pyramidFrameMappings ] ;
357395
358396 /*
359397 * Define custom tile URL function to retrive frames via DICOMweb
360398 * WADO-RS.
361399 */
362- function tileUrlFunction ( tileCoord , pixelRatio , projection ) {
400+ const tileUrlFunction = ( tileCoord , pixelRatio , projection ) => {
363401 /*
364402 * Variables x and y correspond to the X and Y axes of the slide
365403 * coordinate system. Since we want to view the slide horizontally
@@ -378,14 +416,14 @@ class VLWholeSlideMicroscopyImageViewer {
378416 */
379417 let x = - ( tileCoord [ 2 ] + 1 ) + 1 ;
380418 let index = x + "-" + y ;
381- let path = pyramid [ z ] . frameMapping [ index ] ;
419+ let path = pyramidFrameMappings [ z ] [ index ] ;
382420 if ( path === undefined ) {
383421 console . warn ( "tile " + index + " not found at level " + z ) ;
384422 return ( null ) ;
385423 }
386424 let url = options . client . wadoURL +
387- "/studies/" + pyramid [ z ] . studyInstanceUID +
388- "/series/" + pyramid [ z ] . seriesInstanceUID +
425+ "/studies/" + pyramid [ z ] . StudyInstanceUID +
426+ "/series/" + pyramid [ z ] . SeriesInstanceUID +
389427 '/instances/' + path ;
390428 if ( options . retrieveRendered ) {
391429 url = url + '/rendered' ;
@@ -397,20 +435,20 @@ class VLWholeSlideMicroscopyImageViewer {
397435 * Define custonm tile loader function, which is required because the
398436 * WADO-RS response message has content type "multipart/related".
399437 */
400- function base64Encode ( data ) {
438+ const base64Encode = ( data ) => {
401439 const uint8Array = new Uint8Array ( data ) ;
402440 const chunkSize = 0x8000 ;
403441 const strArray = [ ] ;
404442 for ( let i = 0 ; i < uint8Array . length ; i += chunkSize ) {
405- let str = String . fromCharCode . apply (
443+ const str = String . fromCharCode . apply (
406444 null , uint8Array . subarray ( i , i + chunkSize )
407445 ) ;
408446 strArray . push ( str ) ;
409447 }
410448 return btoa ( strArray . join ( '' ) ) ;
411449 }
412450
413- function tileLoadFunction ( tile , src ) {
451+ const tileLoadFunction = ( tile , src ) => {
414452 if ( src !== null ) {
415453 const studyInstanceUID = DICOMwebClient . utils . getStudyInstanceUIDFromUri ( src ) ;
416454 const seriesInstanceUID = DICOMwebClient . utils . getSeriesInstanceUIDFromUri ( src ) ;
@@ -478,8 +516,8 @@ class VLWholeSlideMicroscopyImageViewer {
478516 */
479517 var degrees = 0 ;
480518 if (
481- ( this [ _pyramidBase ] . imageOrientationSlide [ 1 ] === - 1 ) &&
482- ( this [ _pyramidBase ] . imageOrientationSlide [ 3 ] === - 1 )
519+ ( this [ _pyramidBaseMetadata ] . ImageOrientationSlide [ 1 ] === - 1 ) &&
520+ ( this [ _pyramidBaseMetadata ] . ImageOrientationSlide [ 3 ] === - 1 )
483521 ) {
484522 /*
485523 * The row direction (left to right) of the total pixel matrix
@@ -512,7 +550,7 @@ class VLWholeSlideMicroscopyImageViewer {
512550 * DICOM pixel spacing has millimeter unit while the projection has
513551 * has meter unit.
514552 */
515- let spacing = pyramid [ nLevels - 1 ] . pixelSpacing [ 0 ] / 10 ** 3 ;
553+ let spacing = _getPixelSpacing ( pyramid [ nLevels - 1 ] ) [ 0 ] / 10 ** 3 ;
516554 let res = pixelRes * spacing ;
517555 return ( res ) ;
518556 }
@@ -677,23 +715,23 @@ class VLWholeSlideMicroscopyImageViewer {
677715 const container = this [ _map ] . getTargetElement ( ) ;
678716
679717 this [ _drawingSource ] . on ( VectorEventType . ADDFEATURE , ( e ) => {
680- publish ( container , EVENT . ROI_ADDED , _getROIFromFeature ( e . feature , this . _pyramid ) ) ;
718+ publish ( container , EVENT . ROI_ADDED , _getROIFromFeature ( e . feature , this [ _pyramidMetadata ] ) ) ;
681719 } ) ;
682720
683721 this [ _drawingSource ] . on ( VectorEventType . CHANGEFEATURE , ( e ) => {
684- publish ( container , EVENT . ROI_MODIFIED , _getROIFromFeature ( e . feature , this . _pyramid ) ) ;
722+ publish ( container , EVENT . ROI_MODIFIED , _getROIFromFeature ( e . feature , this [ _pyramidMetadata ] ) ) ;
685723 } ) ;
686724
687725 this [ _drawingSource ] . on ( VectorEventType . REMOVEFEATURE , ( e ) => {
688- publish ( container , EVENT . ROI_REMOVED , _getROIFromFeature ( e . feature , this . _pyramid ) ) ;
726+ publish ( container , EVENT . ROI_REMOVED , _getROIFromFeature ( e . feature , this [ _pyramidMetadata ] ) ) ;
689727 } ) ;
690728
691729 this [ _map ] . on ( MapEventType . MOVESTART , ( e ) => {
692- publish ( container , EVENT . DICOM_MOVE_STARTED , this . getAllROIs ( ) ) ;
730+ publish ( container , EVENT . MOVE_STARTED , this . getAllROIs ( ) ) ;
693731 } ) ;
694732
695733 this [ _map ] . on ( MapEventType . MOVEEND , ( e ) => {
696- publish ( container , EVENT . DICOM_MOVE_ENDED , this . getAllROIs ( ) ) ;
734+ publish ( container , EVENT . MOVE_ENDED , this . getAllROIs ( ) ) ;
697735 } ) ;
698736
699737 }
@@ -758,7 +796,7 @@ class VLWholeSlideMicroscopyImageViewer {
758796 //attaching openlayers events handling
759797 this [ _interactions ] . draw . on ( 'drawend' , ( e ) => {
760798 e . feature . setId ( generateUID ( ) ) ;
761- publish ( container , EVENT . ROI_DRAWN , _getROIFromFeature ( e . feature , this . _pyramid ) ) ;
799+ publish ( container , EVENT . ROI_DRAWN , _getROIFromFeature ( e . feature , this [ _pyramidMetadata ] ) ) ;
762800 } ) ;
763801
764802 this [ _map ] . addInteraction ( this [ _interactions ] . draw ) ;
@@ -789,7 +827,7 @@ class VLWholeSlideMicroscopyImageViewer {
789827 const container = this [ _map ] . getTargetElement ( ) ;
790828
791829 this [ _interactions ] . select . on ( 'select' , ( e ) => {
792- publish ( container , EVENT . ROI_SELECTED , _getROIFromFeature ( e . selected [ 0 ] , this . _pyramid ) ) ;
830+ publish ( container , EVENT . ROI_SELECTED , _getROIFromFeature ( e . selected [ 0 ] , this [ _pyramidMetadata ] ) ) ;
793831 } ) ;
794832
795833 this [ _map ] . addInteraction ( this [ _interactions ] . select ) ;
@@ -845,16 +883,16 @@ class VLWholeSlideMicroscopyImageViewer {
845883
846884 getROI ( uid ) {
847885 const feature = this [ _drawingSource ] . getFeatureById ( uid ) ;
848- return _getROIFromFeature ( feature , this . _pyramid ) ;
886+ return _getROIFromFeature ( feature , this [ _pyramidMetadata ] ) ;
849887 }
850888
851889 popROI ( ) {
852890 const feature = this [ _features ] . pop ( ) ;
853- return _getROIFromFeature ( feature , this . _pyramid ) ;
891+ return _getROIFromFeature ( feature , this [ _pyramidMetadata ] ) ;
854892 }
855893
856894 addROI ( item ) {
857- const geometry = _scoord3d2Geometry ( item . scoord3d , this . _pyramid ) ;
895+ const geometry = _scoord3d2Geometry ( item . scoord3d , this [ _pyramidMetadata ] ) ;
858896 const feature = new Feature ( geometry ) ;
859897 feature . setProperties ( item . properties , true ) ;
860898 feature . setId ( item . uid ) ;
@@ -881,6 +919,11 @@ class VLWholeSlideMicroscopyImageViewer {
881919 get areROIsVisible ( ) {
882920 return this [ _drawingLayer ] . getVisible ( ) ;
883921 }
922+
923+ get imageMetadata ( ) {
924+ return this [ _pyramidMetadata ] . reverse ( ) ;
925+ }
926+
884927}
885928
886929export { VLWholeSlideMicroscopyImageViewer } ;
0 commit comments