forked from ome/omero-iviewer
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathViewer.js
More file actions
2474 lines (2227 loc) · 91.8 KB
/
Copy pathViewer.js
File metadata and controls
2474 lines (2227 loc) · 91.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
//
// Copyright (C) 2019 University of Dundee & Open Microscopy Environment.
// All rights reserved.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
import OlObject from 'ol/Object';
import MapEventType from 'ol/MapEventType';
import Geometry from 'ol/geom/Geometry';
import GeometryType from 'ol/geom/GeometryType';
import Polygon from 'ol/geom/Polygon';
import {fromExtent as polygonFromExtent} from 'ol/geom/Polygon.js';
import {listen, unlistenByKey} from 'ol/events';
import Collection from 'ol/Collection';
import Feature from 'ol/Feature';
import Projection from 'ol/proj/Projection';
import Tile from 'ol/layer/Tile';
import Vector from 'ol/layer/Vector';
import View from 'ol/View';
import OlMap from 'ol/Map';
import {intersects, getCenter} from 'ol/extent';
import {noModifierKeys, primaryAction} from 'ol/events/condition';
import Draw from './interaction/Draw';
import ShapeEditPopup from './controls/ShapeEditPopup';
import {checkAndSanitizeServerAddress,
sendRequest} from './utils/Net';
import {generateRegions} from './utils/Regions';
import {modifyStyles,
updateStyleFunction} from './utils/Style';
import Label from './geom/Label';
import {AVAILABLE_VIEWER_INTERACTIONS,
AVAILABLE_VIEWER_CONTROLS,
WEBGATEWAY,
PLUGIN_PREFIX,
DEFAULT_TILE_DIMS,
REGIONS_MODE,
REGIONS_STATE,
DIMENSION_LOOKUP,
PREFIXED_URIS,
defaultInteractions,
defaultControls} from './globals';
import {isArray,
parseProjectionParameter,
parseChannelParameters,
sendEventNotification,
prepareResolutions,
getTargetId} from './utils/Misc';
import {integrateStyleIntoJsonObject,
integrateMiscInfoIntoJsonObject,
toJsonObject,
featureToJsonObject,
LOOKUP} from './utils/Conversion';
import OmeroImage from './source/Image';
import Regions from './source/Regions';
import Mask from './geom/Mask';
import Mirror from './controls/Mirror';
import Grid from './controls/Grid';
import { REQUEST_PARAMS } from '../../utils/constants';
/**
* @classdesc
* Viewer is the central object to view images served by the Omero Server.
* In its simplest form it takes an id to display the associated image:
*
* <pre>
* var omeImgViewer = new Viewer(1);
* </pre>
*
* The constructor takes an object as its second parameter to set further options.
* You can override the server location (server) which by default is relative (same origin)
* as well as the html element which contains the viewer (default: 'ome_ol3_viewer')
* Furthermore you can hand in an EventBus instance to publish/subscribe to
* events for other ui components
*
* e.g.
* <pre>
* var omeImgViewer = new Viewer(1
* { eventbus: eventbus_instance,
* server: 'https://myomeroserver',
* initParams : {'m' : 'c'},
* container: 'somedivsid'});
*</pre>
*
* Moreover, by default the viewer does not add any controls to the viewer.
* As far as interactions go, the only ones enabled, by default, are pan & zoom.
*
* The controls and interactions can be set programmatically by calling the respective methods,
* such as:
*<pre>
* omeImgViewer.addControl("fullscreen");
* omeImgViewer.addInteraction("draw");
*</pre>
*
* @extends {ol.Object}
*/
class Viewer extends OlObject {
/**
* @constructor
*
* @param {number} id an image id
* @param {Object.<string, *>=} options additional properties (optional)
*/
constructor(id, options) {
super();
var opts = options || {};
/**
* the image id
*
* @type {number}
* @private
*/
this.id_ = id || -1;
try {
this.id_ = parseInt(this.id_);
} catch(not_a_number) {
id = -1;
}
/**
* an omero server address given as a fully qualified address
* https://some_host:[some_port]
*
* if not supplied we use relative to what our location is which is the best
* solution for same origin anyhow.
*
* after input sanitization we end up with a server info object
*
* @type {Object}
* @private
*/
this.server_ = checkAndSanitizeServerAddress(opts['server'] || "");
/**
* some initial values/parameters for channel, model and projection (optional)
*
* @type {Object}
* @private
*/
this.initParams_ = opts['initParams'] || {};
/**
* a list of (possibly prefixed) uris for lookup
* @type {Object}
* @private
*/
this.prefixed_uris_ = {};
this.readPrefixedUris(this.initParams_);
/**
* because of async wait until map has been instatiated, an addRegions call
* might not be able to execute successfully. this flag tells us so that
* we can make one once the map initialization has finished
* @type {boolean}
* @private
*/
this.tried_regions_ = false;
/**
* any handed in regions data when we tried the regions (see tried_regions_)
* @type {Array.<Object>}
* @private
*/
this.tried_regions_data_ = false;
/**
* the viewer's sync group
* @type {string|null}
* @private
*/
this.sync_group_ = null;
/**
* the id of the element serving as the container for the viewer
* @type {string}
* @private
*/
this.container_ = "ome-viewer";
if (typeof(opts['container']) === 'string')
this.container_ = opts['container'];
/**
* the associated image information as retrieved from the omero server
* @type {Object}
* @private
*/
this.image_info_ = typeof opts['data'] === 'object' ? opts['data'] : null;
/**
* the associated OmeroRegions object
* @type {Regions}
* @private
*/
this.regions_ = null;
/**
* this flag determines whether ShapeEditPopup is shown on selected shape
* Defauls to true
* @type {boolean}
*/
this.enable_shape_popup = true;
/**
* this flag determines whether a spinner is shown when loading image data
* Defauls to true
* @type {boolean}
*/
this.spinner_enabled = true;
/**
* the 'viewer state', i.e. the controls and interactions that were added
* the items in the map have the following layout
* <pre>
* "key" : { type : "interaction/control", ref : reference_to_instance}
* </pre>
*
* IMPORTANT:
* <ul>
* <li>The key has to be the same as in {@link AVAILABLE_VIEWER_INTERACTIONS} or
* {@link AVAILABLE_VIEWER_CONTROLS}</li>
* <li>The type has to be: 'interaction' or 'control'</li>
* <li>The reference has to exist so that the component can be individually unregistered</li>
* </ul>
*
* @type {Object}
* @private
*/
this.viewerState_ = {};
/**
* the viewer instance
*
* @type {ol.Map}
* @private
*/
this.viewer_ = null;
/**
* an EventBus instance
*
* @type {EventBus}
* @private
*/
this.eventbus_ =
typeof opts['eventbus'] === 'object' &&
typeof opts['eventbus']['publish'] === 'function' ?
opts['eventbus'] : null;
/**
* a flag to indicate the no events should be sent
*
* @type {boolean}
* @private
*/
this.prevent_event_notification_ = false;
/**
* Checks whether a given omero server version is supported.
* It is if the given server version is equal or greater to the
* presently used omero server.
* The given version is accepted in 2 notations:
* - a three digit number (omitting the dots)
* - a string (including dots or not)
*
* @function
* @param {string|number} version the minimal version to check against
* @return {boolean} true if the given version is suppored, false otherwise
*/
this.supportsOmeroServerVersion = function(version) {
if (typeof version === 'number') {
version = '' + version;
} else if (typeof version !== 'string') return false;
// strip off dots and check length
version = parseInt(version.replace(/[.]/g, ""));
if (isNaN(version) || ('' + version).length !== 3) return false;
// check against actual version
let actual_version =
this.getInitialRequestParam(REQUEST_PARAMS.OMERO_VERSION);
if (typeof actual_version !== 'string') return false;
actual_version = parseInt(actual_version.replace(/[.]/g, ""));
if (isNaN(actual_version)) return false;
return actual_version >= version;
}
/**
* The initialization function performs the following steps:
* 1. Request image data as json (if not handed in)
* 2. Store the image data internally (if not handed in)
* 3. Bootstrap ol3 map (and associated objects such as view, controls, etc)
*
* Note: Step 3 happens asynchroniously (if data had to be fetched via ajax)
*
* @function
* @param {Object} scope the java script context
* @param {?function} postSuccessHook an optional post success handler
* @param {?function} initHook an optional initialization handler
* @private
*/
this.initialize_ = function(postSuccessHook, initHook) {
// can happen if we instantitate the viewer without id
if (this.id_ < 0) return;
// use handed in image info instead of requesting it
if (this.image_info_ !== null) {
this.bootstrapOpenLayers(postSuccessHook, initHook);
return;
}
// the success handler instantiates the open layers map and controls
var success = function(data) {
if (typeof(data) === 'string') {
try {
data = JSON.parse(data);
} catch(parseError) {
console.error("Failed to parse json response!");
}
}
if (typeof(data) !== 'object') {
console.error("Image Request did not receive proper response!");
return;
}
// store response internally to be able to work with it later
this.image_info_ = data;
// delegate
this.bootstrapOpenLayers(postSuccessHook, initHook);
}.bind(this);
// define request settings
var reqParams = {
"server" : this.getServer(),
"uri" : this.getPrefixedURI(WEBGATEWAY) +
'/imgData/' + this.id_,
"jsonp" : true, // this will only count if we are cross-domain
"success" : success,
"error" : function(error) {
console.error("Error retrieving image info for id: " +
this.id_ +
((error && error.length > 0) ? (" => " + error) : ""));
}.bind(this)
};
// send request
sendRequest(reqParams);
};
// execute initialization function
this.initialize_();
}
/**
* Bootstraps the Openlayers components
* e.g. view, projection, map and any controls/interactions used
*
* @function
* @param {?function} postSuccessHook an optional post success handler
* @param {?function} initHook an optional initialization handler
* @private
*/
bootstrapOpenLayers(postSuccessHook, initHook) {
if (typeof(this.image_info_['size']) === 'undefined') {
console.error("Image Info does not contain size info!");
return;
}
// we might need to run some initialization handler after we have
// received the json respone.
if (typeof initHook === 'function') initHook.call(this);
/*
* get dimensions of image: width,height, z, t and c sizes,
* as well as zoom levels for pyramids
* for openlayers we gotta prepare the resolutions
* as 1 / res and reverse the order
*/
var zoomLevelScaling = null;
var dims = this.image_info_['size'];
if (this.image_info_['zoomLevelScaling']) {
var tmp = [];
for (var r in this.image_info_['zoomLevelScaling']) {
// Data from server. Don't need to check for zero division
var scale = 1 / this.image_info_['zoomLevelScaling'][r];
if (scale <= tmp[tmp.length - 1]) {
// "Resolutions must be in descending order"
// https://github.com/ome/omero-iviewer/issues/358
break;
}
tmp.push(scale);
}
zoomLevelScaling = tmp.reverse();
}
var zoom = zoomLevelScaling ? zoomLevelScaling.length : -1;
// get the initial projection
var initialProjection =
this.getInitialRequestParam(
REQUEST_PARAMS.PROJECTION);
var parsedInitialProjection =
parseProjectionParameter(
initialProjection !== null ?
initialProjection.toLowerCase() :
this.image_info_['rdefs']['projection']);
initialProjection = parsedInitialProjection.projection;
// get the initial model (color/greyscale)
var initialModel =
this.getInitialRequestParam(REQUEST_PARAMS.MODEL);
initialModel =
initialModel !== null ? initialModel :
this.image_info_['rdefs']['model']
var lowerCaseModel = initialModel.toLowerCase()[0];
switch (lowerCaseModel) {
case 'c': initialModel = 'color'; break;
case 'g': initialModel = 'greyscale'; break;
default: initialModel = 'color';
};
// determine the center
var defaultImgCenter = [dims['width'] / 2, -dims['height'] / 2];
// pixel size
var pixelSize =
typeof this.image_info_['pixel_size'] === "object" &&
typeof this.image_info_['pixel_size']['x'] === "number" ?
this.image_info_['pixel_size']['x'] : null;
// instantiate a pixel projection for omero data
var proj = new Projection({
code: 'OMERO',
units: 'pixels',
extent: [0, 0, dims['width'], dims['height']],
metersPerUnit : pixelSize
});
// we might have some requested defaults
var initialTime = this.getInitialRequestParam(REQUEST_PARAMS.TIME);
initialTime = initialTime !== null ? (parseInt(initialTime)-1) :
this.image_info_['rdefs']['defaultT'];
if (initialTime < 0) initialTime = 0;
if (initialTime >= dims.t) initialTime = dims.t-1;
var initialPlane = this.getInitialRequestParam(REQUEST_PARAMS.PLANE);
initialPlane = initialPlane !== null ? (parseInt(initialPlane)-1) :
this.image_info_['rdefs']['defaultZ'];
if (initialPlane < 0) initialPlane = 0;
if (initialPlane >= dims.z) initialPlane = dims.z-1;
var initialCenterX = this.getInitialRequestParam(REQUEST_PARAMS.CENTER_X);
var initialCenterY = this.getInitialRequestParam(REQUEST_PARAMS.CENTER_Y);
let imgCenter;
if (initialCenterX && !isNaN(parseFloat(initialCenterX)) &&
initialCenterY && !isNaN(parseFloat(initialCenterY))) {
initialCenterX = parseFloat(initialCenterX);
initialCenterY = parseFloat(initialCenterY);
// Restrict centre to within image bounds
initialCenterX = Math.min(Math.max(0, initialCenterX), dims['width']);
initialCenterY = Math.min(Math.max(0, initialCenterY), dims['height']);
imgCenter = [initialCenterX, -initialCenterY];
}
var initialChannels = this.getInitialRequestParam(REQUEST_PARAMS.CHANNELS);
var initialMaps = this.getInitialRequestParam(REQUEST_PARAMS.MAPS);
initialChannels = parseChannelParameters(initialChannels, initialMaps);
var enableMirror = this.getInitialRequestParam(REQUEST_PARAMS.ENABLE_MIRROR) === 'True';
// copy needed channels info
var channels = [];
this.image_info_['channels'].forEach(function(oldC, c) {
var newC = {
"active" : oldC['active'],
"label" : typeof oldC['label'] === 'string' ? oldC['label'] : c,
"color" :
typeof oldC['lut'] === 'string' &&
oldC['lut'].length > 0 ? oldC['lut'] : oldC['color'],
"min" : oldC['window']['min'],
"max" : oldC['window']['max'],
"start" : oldC['window']['start'],
"end" : oldC['window']['end']
};
if (typeof oldC['inverted'] === 'boolean')
newC['inverted'] = oldC['inverted'];
if (typeof oldC['family'] === 'string' && oldC['family'] !== "" &&
typeof oldC['coefficient'] === 'number' &&
!isNaN(oldC['coefficient'])) {
newC['family'] = oldC['family'];
newC['coefficient'] = oldC['coefficient'];
}
channels.push(newC);
});
var isTiled =
typeof this.image_info_['tiles'] === 'boolean' &&
this.image_info_['tiles'];
// create an OmeroImage source
var source = new OmeroImage({
server : this.getServer(),
uri : this.getPrefixedURI(WEBGATEWAY),
image: this.id_,
width: dims['width'],
height: dims['height'],
size_t: dims.t,
plane: initialPlane,
time: initialTime,
channels: channels,
resolutions: zoom > 1 ? zoomLevelScaling : [1],
img_proj: parsedInitialProjection,
img_model: initialModel,
tiled: isTiled,
tile_size: isTiled && this.supportsOmeroServerVersion("5.4.4") ?
DEFAULT_TILE_DIMS :
this.image_info_['tile_size'] ?
this.image_info_['tile_size'] : null
});
source.changeChannelRange(initialChannels, false);
var defaultZoom = zoom > 1 ? zoomLevelScaling[0] : 1;
var actualZoom;
var initialZoom = this.getInitialRequestParam(REQUEST_PARAMS.ZOOM);
var possibleResolutions = prepareResolutions(zoomLevelScaling);
if (initialZoom && !isNaN(parseFloat(initialZoom))) {
initialZoom = (1 / (parseFloat(initialZoom) / 100));
var posLen = possibleResolutions.length;
if (posLen > 1) {
if (initialZoom >= possibleResolutions[0])
actualZoom = possibleResolutions[0];
else if (initialZoom <= possibleResolutions[posLen-1])
actualZoom = possibleResolutions[posLen-1];
else {
// find nearest resolution
for (var r=0;r<posLen-1;r++) {
if (initialZoom < possibleResolutions[r+1])
continue;
var d1 =
Math.abs(possibleResolutions[r] - initialZoom);
var d2 =
Math.abs(possibleResolutions[r+1] - initialZoom);
if (d1 < d2)
actualZoom = possibleResolutions[r];
else actualZoom = possibleResolutions[r+1];
break;
}
}
} else {
actualZoom = 1;
}
}
let highestRes = possibleResolutions[possibleResolutions.length-1];
// For Big images, allow zooming in further (from 161% to > 600%)
if (highestRes > 0.5) {
possibleResolutions.push(highestRes/2);
possibleResolutions.push(highestRes/4);
}
// we need a View object for the map
var view = new View({
projection: proj,
center: defaultImgCenter,
extent: [0, -dims['height'], dims['width'], 0],
resolutions : possibleResolutions,
resolution : defaultZoom,
maxZoom: possibleResolutions.length-1
});
// we have a need to keep a list & reference of the controls
// and interactions registered, therefore we need to take a
// slighlty longer route to add them to the map
var defaultInts = defaultInteractions();
var interactions = new Collection();
for (var inter in defaultInts) {
interactions.push(defaultInts[inter]['ref']);
this.viewerState_[inter] = defaultInts[inter];
}
var defaultConts = defaultControls();
var controls = new Collection();
for (var contr in defaultConts) {
controls.push(defaultConts[contr]['ref']);
this.viewerState_[contr] = defaultConts[contr];
}
// finally construct the open layers map object
this.viewer_ = new OlMap({
logo: false,
controls: controls,
interactions: interactions,
layers: [new Tile({source: source})],
target: this.container_,
view: view
});
// enable bird's eye view
var birdsEyeOptions = {
'url': this.getPrefixedURI(WEBGATEWAY) +
'/render_thumbnail/' + this.id_,
'size': [dims['width'], dims['height']],
'collapsed': !source.use_tiled_retrieval_
};
this.addControl('birdseye', birdsEyeOptions);
// add mirror if requested
if(enableMirror){
var initialFlipX = this.getInitialRequestParam(REQUEST_PARAMS.FLIP_X) === 'true';
var initialFlipY = this.getInitialRequestParam(REQUEST_PARAMS.FLIP_Y) === 'true'
view.setProperties({flipX: false, flipY: false})
// use cached mirror settings if available
if (this.image_info_['flipX']) initialFlipX = this.image_info_['flipX']
if (this.image_info_['flipY']) initialFlipY = this.image_info_['flipY']
this.addControl('mirror', {
flipX: initialFlipX,
flipY: initialFlipY
})
}
// link the grid control to the viewer if requested
this.addControl('grid');
// tweak source element for fullscreen to include dim sliders (iviewer only)
var targetId = this.getTargetId();
var viewerFrame = targetId ? document.getElementById(targetId) : null;
if (targetId && viewerFrame) {
this.viewerState_["fullscreen"]['ref'].source_ = viewerFrame;
this.viewerState_["dragPan"]['ref'].condition_ =
function(e) {
// ignore right clicks (from context)
return noModifierKeys(e) && primaryAction(e);
};
}
// enable scalebar by default
this.toggleScaleBar(true);
// enable intensity control
this.toggleIntensityControl(true);
// helper to broadcast a viewer interaction (zoom, drag, and flip)
var notifyAboutViewerInteraction = function(viewer) {
sendEventNotification(
viewer, "IMAGE_VIEWER_INTERACTION", viewer.getViewParameters());
};
// get cached initial viewer center etc.
if (this.image_info_['center'] || imgCenter || this.image_info_['resolution']
|| this.image_info_['rotation'] || actualZoom) {
let center = this.image_info_['center'] || imgCenter;
let resolution = this.image_info_['resolution'] || actualZoom;
let rotation = this.image_info_['rotation'];
// Need to wait for viewer to be built before this works:
setTimeout(function() {
this.setViewParameters(center, resolution, rotation);
}.bind(this), 100)
}
// listen for rendercomplete to remove the map_spinner
this.renderCompleteListener = listen(
this.viewer_, "rendercomplete",
function(event) {
this.hideSpinner();
if (this.eventbus_) {
sendEventNotification(this, "RENDER_COMPLETE");
}
}, this);
// listen for any tile loading errors...
this.tileLoadErrorListener = listen(
this.getImageLayer().getSource(), "tileloaderror",
function(event) {
if (this.eventbus_) {
sendEventNotification(this, "TILE_LOAD_ERROR");
}
}, this);
// listens to resolution changes
this.onViewResolutionListener =
listen( // register a resolution handler for zoom display
this.viewer_.getView(), "change:resolution",
function(event) {
this.displayResolutionInPercent();
if (this.eventbus_) notifyAboutViewerInteraction(this);
}, this);
this.displayResolutionInPercent();
// listen to rotation changes
this.onViewRotationListener =
listen(
this.viewer_.getView(), "change:rotation",
function(event) {
var regions = this.getRegions();
if (regions) regions.changed();
if (this.eventbus_) notifyAboutViewerInteraction(this);
}, this);
this.onViewFlipXListener =
listen( // register a resolution handler for zoom display
this.viewer_.getView(), "change:flipX",
function(event) {
if (this.eventbus_) notifyAboutViewerInteraction(this);
}, this);
this.onViewFlipYListener =
listen( // register a resolution handler for zoom display
this.viewer_.getView(), "change:flipY",
function(event) {
if (this.eventbus_) notifyAboutViewerInteraction(this);
}, this);
// this is for work that needs to be done after,
// e.g we have just switched images
// because of the asynchronious nature of the initialization
// we need to do this here
if (typeof(postSuccessHook) === 'function')
postSuccessHook.call(this);
if (this.tried_regions) this.addRegions();
if (this.eventbus_) {
// an endMove listener to publish move events
this.onEndMoveListener =
listen(
this.viewer_, MapEventType.MOVEEND,
function(event) {
notifyAboutViewerInteraction(this);
}, this);
}
}
/**
* Shows the viewer
*/
show() {
var viewerElement = document.getElementById(this.container_);
if (viewerElement) viewerElement.style.visibility = "visible";
}
/**
* Hides the viewer
*/
hide() {
var viewerElement = document.getElementById(this.container_);
if (viewerElement) viewerElement.style.visibility = "hidden";
}
/**
* Creates an OmeroRegions instance and stores its reference internally
* with everything that that entails, i.e. an omero server request for rois.
*
* Note, however, that for general drawing ability this method has to be called
* before any drawing interaction is possible regardless of whether the image
* has existing rois associated with it or not!
*
* Important: Calling this method twice or more times will have no effect if
* there is a regions instance present already.
*
* Note: Because of asynchronous viewer initialization this method can be called
* at a moment in time that the viewer has not been fully initialized in which
* case we make a note (flag: tried_regions_) and remember any handed in
* regions data which will be picked up at the end of the initialization process
* when the tried_regions_ flag is checked.
*
* Should you want to hide the regions, once created, call:
* [setRegionsVisibility]{@link Viewer#setRegionsVisibility} passing in: false
*
* If, indeed, you wish to remove the regions layer use:
* [removeRegions]{@link Viewer#removeRegions} but bear in mind that this requires a call to
* [addRegions]{@link Viewer#addRegions} again if you want it back which is more expensive
* than toggling visibility
*
* @param {Array=} data regions data (optional)
*/
addRegions(data) {
// without a map, no need for a regions overlay...
if (!(this.viewer_ instanceof OlMap)) {
this.tried_regions_ = true;
if (isArray(data))
this.tried_regions_data_ = data;
return;
}
this.tried_regions_ = false;
this.tried_regions_data_ = null;
if (this.regions_ instanceof Regions) return;
var options = {};
if (data) options['data'] = data;
// Regions constructor creates ol.Features from JSON data
this.regions_ = new Regions(this, options);
// add a vector layer with the regions
if (this.regions_) {
this.viewer_.addLayer(new Vector({source : this.regions_}));
// enable roi selection by default,
// as well as modify and translate
this.regions_.setModes(
[REGIONS_MODE['SELECT'],
REGIONS_MODE['MODIFY'],
REGIONS_MODE['TRANSLATE']]);
}
//Overlay to show a popup for editing shapes (adds itself to map)
new ShapeEditPopup(this.regions_);
}
/**
* Toggles the visibility of the regions/layer.
* If a non-empty array of rois is handed in, only the listed regions will be affected,
* otherwise the entire layer
*
* @param {boolean} visible visibitily flag (true for visible)
* @param {Array<string>} roi_shape_ids a list of string ids of the form: roi_id:shape_id
*/
setRegionsVisibility(visible, roi_shape_ids) {
// without a regions layer there will be no regions to hide...
var regionsLayer = this.getRegionsLayer();
if (regionsLayer) {
var flag = visible || false;
if (!isArray(roi_shape_ids) || roi_shape_ids.length === 0)
regionsLayer.setVisible(flag);
else
this.getRegions().setProperty(roi_shape_ids, "visible", flag);
}
}
/**
* Toggles the visibility of the regions/layer.
* If a non-empty array of rois is handed in, only the listed regions will be affected,
* otherwise the entire layer
*
* @param {boolean} flag if true we are displaying the text (if there), otherwise no
*/
showShapeComments(flag) {
// without a regions layer there will be no regions to hide...
var regions = this.getRegions();
if (regions && typeof flag === 'boolean') {
regions.show_comments_ = flag;
regions.changed();
}
}
/**
* Enable or disable the showing of a Popup to edit selected shapes.
*
* @param {boolean} flag
*/
enableShapePopup(flag) {
this.enable_shape_popup = flag;
// If enabling, try to show popup
if (flag) {
this.viewer_.getOverlays().forEach(o => {
if (o.updatePopupVisibility) {
o.updatePopupVisibility();
}
});
}
}
/**
* Marks given shapes as selected, clearing any previously selected if clear flag
* is set. Optionally the view centers on a given shape.
*
* @param {Array<string>} roi_shape_ids list in roi_id:shape_id notation
* @param {boolean} selected flag whether we should (de)select the rois
* @param {boolean} clear flag whether we should clear existing selection beforehand
* @param {string|null} panToShape the id of the shape to pan into view or null
* @param {boolean} zoomToShape if true (and panToShape is specified) zoom it into view
*/
selectShapes(roi_shape_ids, selected, clear, panToShape, zoomToShape) {
// without a regions layer there will be no select of regions ...
var regions = this.getRegions();
if (regions === null || regions.select_ === null) return;
if (typeof clear === 'boolean' && clear) regions.select_.clearSelection();
regions.setProperty(roi_shape_ids, "selected", selected);
if (typeof regions.idIndex_[panToShape] === 'object') {
let geom = regions.idIndex_[panToShape].getGeometry();
let target_res;
let forceCentre = false;
if (zoomToShape) {
let extent = geom.getExtent();
// extent is [x, -y, x2, -y2]
let width = extent[2] - extent[0];
let height = extent[3] - extent[1];
let length = Math.max(width, height);
// Zoom till shape is 300px on screen (or until we reach 100%)
target_res = Math.max(length / 300, 1);
// Don't zoom out from current resolution
var res = this.viewer_.getView().getResolution();
// If we zoom in, make sure we centre on shape
if (target_res < res) {
forceCentre = true;
}
target_res = Math.min(target_res, res);
}
this.centerOnGeometry(geom, target_res, forceCentre);
}
}
/**
* Marks given shapes as selected. The center flag is only considered if we
* have a single shape only
*
* @param {Array<string>} roi_shape_ids list in roi_id:shape_id notation
* @param {boolean} undo if true we roll back, default: false
* @param {function=} callback a success handler
*/
deleteShapes(roi_shape_ids, undo, callback) {
// without a regions layer there will be no select of regions ...
var regions = this.getRegions();
if (regions === null) return;
regions.setProperty(
roi_shape_ids, "state",
typeof undo === 'boolean' && undo ?
REGIONS_STATE.ROLLBACK : REGIONS_STATE.REMOVED,
typeof callback === 'function' ? callback : null);
}
/**
* Centers view on the middle of a geometry
* unless geometry is already in viewport,
* optionally zooming in on a given resolution
*
* @param {ol.geom.Geometry} geometry the geometry
* @param {number=} resolution the resolution to zoom in on
* @param {bool} forceCentre if true, ALWAYS centre
*/
centerOnGeometry(geometry, resolution, forceCentre) {
if (!(geometry instanceof Geometry)) return;
// use given resolution for zoom
if (typeof resolution === 'number' && !isNaN(resolution)) {
var constrainedResolution =
this.viewer_.getView().constrainResolution(resolution);
if (typeof constrainedResolution === 'number')
this.viewer_.getView().setResolution(constrainedResolution);
}
// only center if we don't intersect the viewport after zooming
if (intersects(
geometry.getExtent(),
this.viewer_.getView().calculateExtent()) && (!forceCentre)) return;
// center (taking into account potential rotation)
var rot = this.viewer_.getView().getRotation();
if (geometry.getType() === GeometryType.CIRCLE) {
var ext = geometry.getExtent();
geometry = polygonFromExtent(ext);
geometry.rotate(rot, getCenter(ext));
}
var coords = geometry.getFlatCoordinates();
var cosine = Math.cos(-rot);
var sine = Math.sin(-rot);
var minRotX = +Infinity;
var minRotY = +Infinity;
var maxRotX = -Infinity;
var maxRotY = -Infinity;
var stride = geometry.getStride();
for (var i = 0, ii = coords.length; i < ii; i += stride) {
var rotX = coords[i] * cosine - coords[i + 1] * sine;
var rotY = coords[i] * sine + coords[i + 1] * cosine;
minRotX = Math.min(minRotX, rotX);
minRotY = Math.min(minRotY, rotY);
maxRotX = Math.max(maxRotX, rotX);
maxRotY = Math.max(maxRotY, rotY);
}
sine = -sine;
var centerRotX = (minRotX + maxRotX) / 2;
var centerRotY = (minRotY + maxRotY) / 2;
var centerX = centerRotX * cosine - centerRotY * sine;
var centerY = centerRotY * cosine + centerRotX * sine;
this.viewer_.getView().setCenter([centerX, centerY]);
}
/**
* Removes the regions from the viewer which is a multi step procedure
* that involves destroying the OmeroRegions instance as well as the open
* layer's vector layer
*
*/
removeRegions() {
// without an existing instance no need to destroy it...
if (this.regions_ instanceof Regions) {
// reset mode which will automatically deregister interactions
this.regions_.setModes([REGIONS_MODE['DEFAULT']]);
// dispose of the internal OmeroRegions instance
this.regions_.dispose();
this.regions_ = null;
}