From e09148438b45ae14b89aaa542f3feb9560bf0bf2 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Mon, 11 May 2026 07:17:11 +0200 Subject: [PATCH 1/7] Initial commit for vibe-coded segmentation mode. --- .../migrations/0050_segmentation_tile.py | 30 + exact/exact/annotations/models.py | 34 +- .../annotations/js/exact-image-viewer.js | 6 +- .../js/openseadragon.min.js-current.js | 9 + .../js/openseadragon.min.js.map-current | 1 + .../static/annotations/js/segmentationTool.js | 839 ++++++++++++++++++ exact/exact/annotations/urls.py | 3 + exact/exact/annotations/views.py | 103 ++- 8 files changed, 1020 insertions(+), 5 deletions(-) create mode 100644 exact/exact/annotations/migrations/0050_segmentation_tile.py create mode 100644 exact/exact/annotations/static/annotations/js/openseadragon.min.js-current.js create mode 100644 exact/exact/annotations/static/annotations/js/openseadragon.min.js.map-current create mode 100644 exact/exact/annotations/static/annotations/js/segmentationTool.js diff --git a/exact/exact/annotations/migrations/0050_segmentation_tile.py b/exact/exact/annotations/migrations/0050_segmentation_tile.py new file mode 100644 index 00000000..11fc15f0 --- /dev/null +++ b/exact/exact/annotations/migrations/0050_segmentation_tile.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.18 on 2026-05-10 09:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('annotations', '0049_annotationtype_multi_frame'), + ] + + operations = [ + migrations.CreateModel( + name='SegmentationTile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('level', models.IntegerField()), + ('tile_x', models.IntegerField()), + ('tile_y', models.IntegerField()), + ('frame', models.IntegerField(default=0)), + ('data', models.BinaryField()), + ('annotation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='segmentation_tiles', to='annotations.annotation')), + ], + options={ + 'indexes': [models.Index(fields=['annotation', 'level', 'frame'], name='annotations_annotat_9dba0a_idx')], + 'unique_together': {('annotation', 'level', 'tile_x', 'tile_y', 'frame')}, + }, + ), + ] diff --git a/exact/exact/annotations/models.py b/exact/exact/annotations/models.py index 70aaa81b..c508148e 100644 --- a/exact/exact/annotations/models.py +++ b/exact/exact/annotations/models.py @@ -453,6 +453,7 @@ class VECTOR_TYPE(): POLYGON = 5 FIXED_SIZE_BOUNDING_BOX = 6 GLOBAL = 7 #Annotations without a shape that are valid for the whole image + SEGMENTATION = 8 # Pixel-level mask stored as compressed tiles name = models.CharField(max_length=50) active = models.BooleanField(default=True) @@ -494,6 +495,8 @@ def get_vector_type_name(vector_type): return 'Multi Line' if vector_type is AnnotationType.VECTOR_TYPE.FIXED_SIZE_BOUNDING_BOX: return 'Fixed Size Bounding Box' + if vector_type is AnnotationType.VECTOR_TYPE.SEGMENTATION: + return 'Segmentation' def validate_vector(self, vector: Union[dict, None]) -> bool: """ @@ -525,6 +528,8 @@ def validate_vector(self, vector: Union[dict, None]) -> bool: return self._validate_bounding_box(vector) if self.vector_type == AnnotationType.VECTOR_TYPE.GLOBAL: return self._validate_bounding_box(vector) + if self.vector_type == AnnotationType.VECTOR_TYPE.SEGMENTATION: + return True # tile metadata; pixel data lives in SegmentationTile # No valid vector type given. return False @@ -692,4 +697,31 @@ class MediaFileType(IntEnum): related_query_name="uploaded_media_file") def __str__(self): - return self.name + ": " + str(self.file) \ No newline at end of file + return self.name + ": " + str(self.file) + + +class SegmentationTile(models.Model): + """Stores a single compressed PNG tile of a pixel-level segmentation mask. + + Tiles are addressed by (annotation, level, tile_x, tile_y, frame) and + stored as 8-bit palette-mode PNG where each pixel value is a class label + (0 = background/transparent). Missing tiles are implicitly empty, giving + sparse storage that scales to gigapixel WSIs. + """ + + class Meta: + unique_together = ('annotation', 'level', 'tile_x', 'tile_y', 'frame') + indexes = [ + models.Index(fields=['annotation', 'level', 'frame']), + ] + + annotation = models.ForeignKey( + Annotation, on_delete=models.CASCADE, related_name='segmentation_tiles') + level = models.IntegerField() # 0 = full resolution, higher = more zoomed-out + tile_x = models.IntegerField() # column index in the tile grid at this level + tile_y = models.IntegerField() # row index + frame = models.IntegerField(default=0) # z-slice / time index for volumetric images + data = models.BinaryField() # PNG-encoded palette image (class label per pixel) + + def __str__(self): + return f'SegmentationTile ann={self.annotation_id} lv={self.level} ({self.tile_x},{self.tile_y}) frame={self.frame}' \ No newline at end of file diff --git a/exact/exact/annotations/static/annotations/js/exact-image-viewer.js b/exact/exact/annotations/static/annotations/js/exact-image-viewer.js index c636092e..728337ab 100644 --- a/exact/exact/annotations/static/annotations/js/exact-image-viewer.js +++ b/exact/exact/annotations/static/annotations/js/exact-image-viewer.js @@ -27,8 +27,10 @@ class EXACTViewer { this.frame = 1; this.viewer = this.createViewer(options); - this.exact_registration_sync = undefined; - this.browser_sync = undefined; + window.exactOSDViewer = this.viewer; // expose for segmentationTool + window.dispatchEvent(new CustomEvent('exactViewerReady', { detail: this.viewer })); + this.exact_registration_sync = undefined; + this.browser_sync = undefined; this.exact_image_sync = new EXACTImageSync(this.imageId, this.gHeaders, this.viewer); this.initViewerEventHandler(this.viewer, imageInformation); diff --git a/exact/exact/annotations/static/annotations/js/openseadragon.min.js-current.js b/exact/exact/annotations/static/annotations/js/openseadragon.min.js-current.js new file mode 100644 index 00000000..b95f5d15 --- /dev/null +++ b/exact/exact/annotations/static/annotations/js/openseadragon.min.js-current.js @@ -0,0 +1,9 @@ +//! openseadragon 2.4.2 +//! Built on 2020-03-05 +//! Git commit: v2.4.2-0-c450749 +//! http://openseadragon.github.io +//! License: http://openseadragon.github.io/license/ + + +function OpenSeadragon(e){return new OpenSeadragon.Viewer(e)}!function(n){n.version={versionStr:"2.4.2",major:parseInt("2",10),minor:parseInt("4",10),revision:parseInt("2",10)};var t={"[object Boolean]":"boolean","[object Number]":"number","[object String]":"string","[object Function]":"function","[object Array]":"array","[object Date]":"date","[object RegExp]":"regexp","[object Object]":"object"},i=Object.prototype.toString,o=Object.prototype.hasOwnProperty;n.isFunction=function(e){return"function"===n.type(e)};n.isArray=Array.isArray||function(e){return"array"===n.type(e)};n.isWindow=function(e){return e&&"object"==typeof e&&"setInterval"in e};n.type=function(e){return null==e?String(e):t[i.call(e)]||"object"};n.isPlainObject=function(e){if(!e||"object"!==OpenSeadragon.type(e)||e.nodeType||n.isWindow(e))return!1;if(e.constructor&&!o.call(e,"constructor")&&!o.call(e.constructor.prototype,"isPrototypeOf"))return!1;var t;for(var i in e)t=i;return void 0===t||o.call(e,t)};n.isEmptyObject=function(e){for(var t in e)return!1;return!0};n.freezeObject=function(e){Object.freeze?n.freezeObject=Object.freeze:n.freezeObject=function(e){return e};return n.freezeObject(e)};n.supportsCanvas=(e=document.createElement("canvas"),!(!n.isFunction(e.getContext)||!e.getContext("2d")));var e;n.isCanvasTainted=function(e){var t=!1;try{e.getContext("2d").getImageData(0,0,1,1)}catch(e){t=!0}return t};n.pixelDensityRatio=function(){if(n.supportsCanvas){var e=document.createElement("canvas").getContext("2d");var t=window.devicePixelRatio||1;var i=e.webkitBackingStorePixelRatio||e.mozBackingStorePixelRatio||e.msBackingStorePixelRatio||e.oBackingStorePixelRatio||e.backingStorePixelRatio||1;return Math.max(t,1)/i}return 1}()}(OpenSeadragon);!function($){$.extend=function(){var e,t,i,n,o,r,s=arguments[0]||{},a=arguments.length,l=!1,h=1;if("boolean"==typeof s){l=s;s=arguments[1]||{};h=2}"object"==typeof s||OpenSeadragon.isFunction(s)||(s={});if(a===h){s=this;--h}for(;h=i.x&&t.x=i.y},getEvent:function(e){$.getEvent=e?function(e){return e}:function(){return window.event};return $.getEvent(e)},getMousePosition:function(e){if("number"==typeof e.pageX)$.getMousePosition=function(e){var t=new $.Point;e=$.getEvent(e);t.x=e.pageX;t.y=e.pageY;return t};else{if("number"!=typeof e.clientX)throw new Error("Unknown event mouse position, no known technique.");$.getMousePosition=function(e){var t=new $.Point;e=$.getEvent(e);t.x=e.clientX+document.body.scrollLeft+document.documentElement.scrollLeft;t.y=e.clientY+document.body.scrollTop+document.documentElement.scrollTop;return t}}return $.getMousePosition(e)},getPageScroll:function(){var e=document.documentElement||{},t=document.body||{};if("number"==typeof window.pageXOffset)$.getPageScroll=function(){return new $.Point(window.pageXOffset,window.pageYOffset)};else if(t.scrollLeft||t.scrollTop)$.getPageScroll=function(){return new $.Point(document.body.scrollLeft,document.body.scrollTop)};else{if(!e.scrollLeft&&!e.scrollTop)return new $.Point(0,0);$.getPageScroll=function(){return new $.Point(document.documentElement.scrollLeft,document.documentElement.scrollTop)}}return $.getPageScroll()},setPageScroll:function(e){if(void 0!==window.scrollTo)$.setPageScroll=function(e){window.scrollTo(e.x,e.y)};else{var t=$.getPageScroll();if(t.x===e.x&&t.y===e.y)return;document.body.scrollLeft=e.x;document.body.scrollTop=e.y;var i=$.getPageScroll();if(i.x!==t.x&&i.y!==t.y){$.setPageScroll=function(e){document.body.scrollLeft=e.x;document.body.scrollTop=e.y};return}document.documentElement.scrollLeft=e.x;document.documentElement.scrollTop=e.y;if((i=$.getPageScroll()).x!==t.x&&i.y!==t.y){$.setPageScroll=function(e){document.documentElement.scrollLeft=e.x;document.documentElement.scrollTop=e.y};return}$.setPageScroll=function(e){}}return $.setPageScroll(e)},getWindowSize:function(){var e=document.documentElement||{},t=document.body||{};if("number"==typeof window.innerWidth)$.getWindowSize=function(){return new $.Point(window.innerWidth,window.innerHeight)};else if(e.clientWidth||e.clientHeight)$.getWindowSize=function(){return new $.Point(document.documentElement.clientWidth,document.documentElement.clientHeight)};else{if(!t.clientWidth&&!t.clientHeight)throw new Error("Unknown window size, no known technique.");$.getWindowSize=function(){return new $.Point(document.body.clientWidth,document.body.clientHeight)}}return $.getWindowSize()},makeCenteredNode:function(e){e=$.getElement(e);var t=[$.makeNeutralElement("div"),$.makeNeutralElement("div"),$.makeNeutralElement("div")];$.extend(t[0].style,{display:"table",height:"100%",width:"100%"});$.extend(t[1].style,{display:"table-row"});$.extend(t[2].style,{display:"table-cell",verticalAlign:"middle",textAlign:"center"});t[0].appendChild(t[1]);t[1].appendChild(t[2]);t[2].appendChild(e);return t[0]},makeNeutralElement:function(e){var t=document.createElement(e),i=t.style;i.background="transparent none";i.border="none";i.margin="0px";i.padding="0px";i.position="static";return t},now:function(){Date.now?$.now=Date.now:$.now=function(){return(new Date).getTime()};return $.now()},makeTransparentImage:function(e){$.makeTransparentImage=function(e){var t=$.makeNeutralElement("img");t.src=e;return t};$.Browser.vendor==$.BROWSERS.IE&&$.Browser.version<7&&($.makeTransparentImage=function(e){var t=$.makeNeutralElement("img"),i=null;(i=$.makeNeutralElement("span")).style.display="inline-block";t.onload=function(){i.style.width=i.style.width||t.width+"px";i.style.height=i.style.height||t.height+"px";t.onload=null;t=null};t.src=e;i.style.filter="progid:DXImageTransform.Microsoft.AlphaImageLoader(src='"+e+"', sizingMethod='scale')";return i});return $.makeTransparentImage(e)},setElementOpacity:function(e,t,i){var n;e=$.getElement(e);i&&!$.Browser.alpha&&(t=Math.round(t));if($.Browser.opacity)e.style.opacity=t<1?t:"";else if(t<1){n="alpha(opacity="+Math.round(100*t)+")";e.style.filter=n}else e.style.filter=""},setElementTouchActionNone:function(e){void 0!==(e=$.getElement(e)).style.touchAction?e.style.touchAction="none":void 0!==e.style.msTouchAction&&(e.style.msTouchAction="none")},addClass:function(e,t){(e=$.getElement(e)).className?-1===(" "+e.className+" ").indexOf(" "+t+" ")&&(e.className+=" "+t):e.className=t},indexOf:function(e,t,i){Array.prototype.indexOf?this.indexOf=function(e,t,i){return e.indexOf(t,i)}:this.indexOf=function(e,t,i){var n,o,r=i||0;if(!e)throw new TypeError;if(0===(o=e.length)||o<=r)return-1;r<0&&(r=o-Math.abs(r));for(n=r;nt.touches.length-s){v.console.warn("Tracked touch contact count doesn't match event.touches.length. Removing all tracked touch pointers.");b(e,t,l)}for(n=0;n\s*$/))o=m.parseXml(o);else if(o.match(/^\s*[\{\[].*[\}\]]\s*$/))try{var e=m.parseJSON(o);o=e}catch(e){}function h(e,t){if(e.ready)s(e);else{e.addHandler("ready",function(){s(e)});e.addHandler("open-failed",function(e){a({message:e.message,source:t})})}}setTimeout(function(){if("string"==m.type(o))(o=new m.TileSource({url:o,crossOriginPolicy:void 0!==r.crossOriginPolicy?r.crossOriginPolicy:n.crossOriginPolicy,ajaxWithCredentials:n.ajaxWithCredentials,ajaxHeaders:n.ajaxHeaders,useCanvas:n.useCanvas,success:function(e){s(e.tileSource)}})).addHandler("open-failed",function(e){a(e)});else if(m.isPlainObject(o)||o.nodeType){void 0!==o.crossOriginPolicy||void 0===r.crossOriginPolicy&&void 0===n.crossOriginPolicy||(o.crossOriginPolicy=void 0!==r.crossOriginPolicy?r.crossOriginPolicy:n.crossOriginPolicy);void 0===o.ajaxWithCredentials&&(o.ajaxWithCredentials=n.ajaxWithCredentials);void 0===o.useCanvas&&(o.useCanvas=n.useCanvas);if(m.isFunction(o.getTileUrl)){var e=new m.TileSource(o);e.getTileUrl=o.getTileUrl;s(e)}else{var t=m.TileSource.determineType(l,o);if(!t){a({message:"Unable to load TileSource",source:o});return}var i=t.prototype.configure.apply(l,[o]);h(new t(i),o)}}else h(o,o)})}(this,i.tileSource,i,function(e){n.tileSource=e;s()},function(e){e.options=i;t(e);s()})}function s(){var e,t,i;for(;o._loadQueue.length&&(e=o._loadQueue[0]).tileSource;){o._loadQueue.splice(0,1);if(e.options.replace){var n=o.world.getIndexOfItem(e.options.replaceItem);-1!=n&&(e.options.index=n);o.world.removeItem(e.options.replaceItem)}t=new m.TiledImage({viewer:o,source:e.tileSource,viewport:o.viewport,drawer:o.drawer,tileCache:o.tileCache,imageLoader:o.imageLoader,x:e.options.x,y:e.options.y,width:e.options.width,height:e.options.height,fitBounds:e.options.fitBounds,fitBoundsPlacement:e.options.fitBoundsPlacement,clip:e.options.clip,placeholderFillStyle:e.options.placeholderFillStyle,opacity:e.options.opacity,preload:e.options.preload,degrees:e.options.degrees,compositeOperation:e.options.compositeOperation,springStiffness:o.springStiffness,animationTime:o.animationTime,minZoomImageRatio:o.minZoomImageRatio,wrapHorizontal:o.wrapHorizontal,wrapVertical:o.wrapVertical,immediateRender:o.immediateRender,blendTime:o.blendTime,alwaysBlend:o.alwaysBlend,minPixelRatio:o.minPixelRatio,smoothTileEdgesMinZoom:o.smoothTileEdgesMinZoom,iOSDevice:o.iOSDevice,crossOriginPolicy:e.options.crossOriginPolicy,ajaxWithCredentials:e.options.ajaxWithCredentials,loadTilesWithAjax:e.options.loadTilesWithAjax,ajaxHeaders:e.options.ajaxHeaders,debugMode:o.debugMode});o.collectionMode&&o.world.setAutoRefigureSizes(!1);o.world.addItem(t,{index:e.options.index});0===o._loadQueue.length&&r(e);1!==o.world.getItemCount()||o.preserveViewport||o.viewport.goHome(!0);if(o.navigator){i=m.extend({},e.options,{replace:!1,originalTiledImage:t,tileSource:e.tileSource});o.navigator.addTiledImage(i)}e.options.success&&e.options.success({item:t})}}},addSimpleImage:function(e){m.console.assert(e,"[Viewer.addSimpleImage] options is required");m.console.assert(e.url,"[Viewer.addSimpleImage] options.url is required");var t=m.extend({},e,{tileSource:{type:"image",url:e.url}});delete t.url;this.addTiledImage(t)},addLayer:function(t){var i=this;m.console.error("[Viewer.addLayer] this function is deprecated; use Viewer.addTiledImage() instead.");var e=m.extend({},t,{success:function(e){i.raiseEvent("add-layer",{options:t,drawer:e.item})},error:function(e){i.raiseEvent("add-layer-failed",e)}});this.addTiledImage(e);return this},getLayerAtLevel:function(e){m.console.error("[Viewer.getLayerAtLevel] this function is deprecated; use World.getItemAt() instead.");return this.world.getItemAt(e)},getLevelOfLayer:function(e){m.console.error("[Viewer.getLevelOfLayer] this function is deprecated; use World.getIndexOfItem() instead.");return this.world.getIndexOfItem(e)},getLayersCount:function(){m.console.error("[Viewer.getLayersCount] this function is deprecated; use World.getItemCount() instead.");return this.world.getItemCount()},setLayerLevel:function(e,t){m.console.error("[Viewer.setLayerLevel] this function is deprecated; use World.setItemIndex() instead.");return this.world.setItemIndex(e,t)},removeLayer:function(e){m.console.error("[Viewer.removeLayer] this function is deprecated; use World.removeItem() instead.");return this.world.removeItem(e)},forceRedraw:function(){c[this.hash].forceRedraw=!0;return this},bindSequenceControls:function(){var e=m.delegate(this,v),t=m.delegate(this,f),i=m.delegate(this,$),n=m.delegate(this,G),o=this.navImages,r=!0;if(this.showSequenceControl){(this.previousButton||this.nextButton)&&(r=!1);this.previousButton=new m.Button({element:this.previousButton?m.getElement(this.previousButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.PreviousPage"),srcRest:D(this.prefixUrl,o.previous.REST),srcGroup:D(this.prefixUrl,o.previous.GROUP),srcHover:D(this.prefixUrl,o.previous.HOVER),srcDown:D(this.prefixUrl,o.previous.DOWN),onRelease:n,onFocus:e,onBlur:t});this.nextButton=new m.Button({element:this.nextButton?m.getElement(this.nextButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.NextPage"),srcRest:D(this.prefixUrl,o.next.REST),srcGroup:D(this.prefixUrl,o.next.GROUP),srcHover:D(this.prefixUrl,o.next.HOVER),srcDown:D(this.prefixUrl,o.next.DOWN),onRelease:i,onFocus:e,onBlur:t});this.navPrevNextWrap||this.previousButton.disable();this.tileSources&&this.tileSources.length||this.nextButton.disable();if(r){this.paging=new m.ButtonGroup({buttons:[this.previousButton,this.nextButton],clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.pagingControl=this.paging.element;this.toolbar?this.toolbar.addControl(this.pagingControl,{anchor:m.ControlAnchor.BOTTOM_RIGHT}):this.addControl(this.pagingControl,{anchor:this.sequenceControlAnchor||m.ControlAnchor.TOP_LEFT})}}return this},bindStandardControls:function(){var e=m.delegate(this,M),t=m.delegate(this,H),i=m.delegate(this,F),n=m.delegate(this,z),o=m.delegate(this,L),r=m.delegate(this,N),s=m.delegate(this,W),a=m.delegate(this,V),l=m.delegate(this,U),h=m.delegate(this,j),c=m.delegate(this,v),u=m.delegate(this,f),d=this.navImages,p=[],g=!0;if(this.showNavigationControl){(this.zoomInButton||this.zoomOutButton||this.homeButton||this.fullPageButton||this.rotateLeftButton||this.rotateRightButton||this.flipButton)&&(g=!1);if(this.showZoomControl){p.push(this.zoomInButton=new m.Button({element:this.zoomInButton?m.getElement(this.zoomInButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomIn"),srcRest:D(this.prefixUrl,d.zoomIn.REST),srcGroup:D(this.prefixUrl,d.zoomIn.GROUP),srcHover:D(this.prefixUrl,d.zoomIn.HOVER),srcDown:D(this.prefixUrl,d.zoomIn.DOWN),onPress:e,onRelease:t,onClick:i,onEnter:e,onExit:t,onFocus:c,onBlur:u}));p.push(this.zoomOutButton=new m.Button({element:this.zoomOutButton?m.getElement(this.zoomOutButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.ZoomOut"),srcRest:D(this.prefixUrl,d.zoomOut.REST),srcGroup:D(this.prefixUrl,d.zoomOut.GROUP),srcHover:D(this.prefixUrl,d.zoomOut.HOVER),srcDown:D(this.prefixUrl,d.zoomOut.DOWN),onPress:n,onRelease:t,onClick:o,onEnter:n,onExit:t,onFocus:c,onBlur:u}))}this.showHomeControl&&p.push(this.homeButton=new m.Button({element:this.homeButton?m.getElement(this.homeButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Home"),srcRest:D(this.prefixUrl,d.home.REST),srcGroup:D(this.prefixUrl,d.home.GROUP),srcHover:D(this.prefixUrl,d.home.HOVER),srcDown:D(this.prefixUrl,d.home.DOWN),onRelease:r,onFocus:c,onBlur:u}));this.showFullPageControl&&p.push(this.fullPageButton=new m.Button({element:this.fullPageButton?m.getElement(this.fullPageButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.FullPage"),srcRest:D(this.prefixUrl,d.fullpage.REST),srcGroup:D(this.prefixUrl,d.fullpage.GROUP),srcHover:D(this.prefixUrl,d.fullpage.HOVER),srcDown:D(this.prefixUrl,d.fullpage.DOWN),onRelease:s,onFocus:c,onBlur:u}));if(this.showRotationControl){p.push(this.rotateLeftButton=new m.Button({element:this.rotateLeftButton?m.getElement(this.rotateLeftButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateLeft"),srcRest:D(this.prefixUrl,d.rotateleft.REST),srcGroup:D(this.prefixUrl,d.rotateleft.GROUP),srcHover:D(this.prefixUrl,d.rotateleft.HOVER),srcDown:D(this.prefixUrl,d.rotateleft.DOWN),onRelease:a,onFocus:c,onBlur:u}));p.push(this.rotateRightButton=new m.Button({element:this.rotateRightButton?m.getElement(this.rotateRightButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.RotateRight"),srcRest:D(this.prefixUrl,d.rotateright.REST),srcGroup:D(this.prefixUrl,d.rotateright.GROUP),srcHover:D(this.prefixUrl,d.rotateright.HOVER),srcDown:D(this.prefixUrl,d.rotateright.DOWN),onRelease:l,onFocus:c,onBlur:u}))}this.showFlipControl&&p.push(this.flipButton=new m.Button({element:this.flipButton?m.getElement(this.flipButton):null,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,tooltip:m.getString("Tooltips.Flip"),srcRest:D(this.prefixUrl,d.flip.REST),srcGroup:D(this.prefixUrl,d.flip.GROUP),srcHover:D(this.prefixUrl,d.flip.HOVER),srcDown:D(this.prefixUrl,d.flip.DOWN),onRelease:h,onFocus:c,onBlur:u}));if(g){this.buttons=new m.ButtonGroup({buttons:p,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold});this.navControl=this.buttons.element;this.addHandler("open",m.delegate(this,A));this.toolbar?this.toolbar.addControl(this.navControl,{anchor:this.navigationControlAnchor||m.ControlAnchor.TOP_LEFT}):this.addControl(this.navControl,{anchor:this.navigationControlAnchor||m.ControlAnchor.TOP_LEFT})}}return this},currentPage:function(){return this._sequenceIndex},goToPage:function(e){if(this.tileSources&&0<=e&&e=t.flickMinSpeed){var i=0;this.panHorizontal&&(i=t.flickMomentum*e.speed*Math.cos(e.direction));var n=0;this.panVertical&&(n=t.flickMomentum*e.speed*Math.sin(e.direction));var o=this.viewport.pixelFromPoint(this.viewport.getCenter(!0));var r=this.viewport.pointFromPixel(new m.Point(o.x-i,o.y-n));this.viewport.panTo(r,!1)}this.viewport.applyConstraints()}this.raiseEvent("canvas-drag-end",{tracker:e.eventSource,position:e.position,speed:e.speed,direction:e.direction,shift:e.shift,originalEvent:e.originalEvent})}function S(e){this.raiseEvent("canvas-enter",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function E(e){window.location!=window.parent.location&&m.MouseTracker.resetAllMouseTrackers();this.raiseEvent("canvas-exit",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function P(e){this.raiseEvent("canvas-press",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,insideElementPressed:e.insideElementPressed,insideElementReleased:e.insideElementReleased,originalEvent:e.originalEvent})}function R(e){this.raiseEvent("canvas-release",{tracker:e.eventSource,pointerType:e.pointerType,position:e.position,insideElementPressed:e.insideElementPressed,insideElementReleased:e.insideElementReleased,originalEvent:e.originalEvent})}function _(e){this.raiseEvent("canvas-nonprimary-press",{tracker:e.eventSource,position:e.position,pointerType:e.pointerType,button:e.button,buttons:e.buttons,originalEvent:e.originalEvent})}function b(e){this.raiseEvent("canvas-nonprimary-release",{tracker:e.eventSource,position:e.position,pointerType:e.pointerType,button:e.button,buttons:e.buttons,originalEvent:e.originalEvent})}function C(e){var t,i,n;if(!e.preventDefaultAction&&this.viewport){if((t=this.gestureSettingsByDeviceType(e.pointerType)).pinchToZoom){i=this.viewport.pointFromPixel(e.center,!0);n=this.viewport.pointFromPixel(e.lastCenter,!0).minus(i);this.panHorizontal||(n.x=0);this.panVertical||(n.y=0);this.viewport.zoomBy(e.distance/e.lastDistance,i,!0);t.zoomToRefPoint&&this.viewport.panBy(n,!0);this.viewport.applyConstraints()}if(t.pinchRotate){var o=Math.atan2(e.gesturePoints[0].currentPos.y-e.gesturePoints[1].currentPos.y,e.gesturePoints[0].currentPos.x-e.gesturePoints[1].currentPos.x);var r=Math.atan2(e.gesturePoints[0].lastPos.y-e.gesturePoints[1].lastPos.y,e.gesturePoints[0].lastPos.x-e.gesturePoints[1].lastPos.x);this.viewport.setRotation(this.viewport.getRotation()+(o-r)*(180/Math.PI))}}this.raiseEvent("canvas-pinch",{tracker:e.eventSource,gesturePoints:e.gesturePoints,lastCenter:e.lastCenter,center:e.center,lastDistance:e.lastDistance,distance:e.distance,shift:e.shift,originalEvent:e.originalEvent});return!1}function O(e){var t,i,n;if((n=m.now())-this._lastScrollTime>this.minScrollDeltaTime){this._lastScrollTime=n;this.viewport.flipped&&(e.position.x=this.viewport.getContainerSize().x-e.position.x);if(!e.preventDefaultAction&&this.viewport&&(t=this.gestureSettingsByDeviceType(e.pointerType)).scrollToZoom){i=Math.pow(this.zoomPerScroll,e.scroll);this.viewport.zoomBy(i,t.zoomToRefPoint?this.viewport.pointFromPixel(e.position,!0):null);this.viewport.applyConstraints()}this.raiseEvent("canvas-scroll",{tracker:e.eventSource,position:e.position,scroll:e.scroll,shift:e.shift,originalEvent:e.originalEvent});if(t&&t.scrollToZoom)return!1}else if((t=this.gestureSettingsByDeviceType(e.pointerType))&&t.scrollToZoom)return!1}function I(e){c[this.hash].mouseInside=!0;g(this);this.raiseEvent("container-enter",{tracker:e.eventSource,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function k(e){if(e.pointers<1){c[this.hash].mouseInside=!1;c[this.hash].animating||p(this)}this.raiseEvent("container-exit",{tracker:e.eventSource,position:e.position,buttons:e.buttons,pointers:e.pointers,insideElementPressed:e.insideElementPressed,buttonDownAny:e.buttonDownAny,originalEvent:e.originalEvent})}function B(e){!function(e){if(e._opening)return;if(e.autoResize){var t=u(e.container);var i=c[e.hash].prevContainerSize;if(!t.equals(i)){var n=e.viewport;if(e.preserveImageSizeOnResize){var o=i.x/t.x;var r=n.getZoom()*o;var s=n.getCenter();n.resize(t,!1);n.zoomTo(r,null,!0);n.panTo(s,!0)}else{var a=n.getBounds();n.resize(t,!0);n.fitBoundsWithConstraints(a,!0)}c[e.hash].prevContainerSize=t;c[e.hash].forceRedraw=!0}}var l=e.viewport.update();var h=e.world.update()||l;l&&e.raiseEvent("viewport-change");e.referenceStrip&&(h=e.referenceStrip.update(e.viewport)||h);if(!c[e.hash].animating&&h){e.raiseEvent("animation-start");g(e)}if(h||c[e.hash].forceRedraw||e.world.needsDraw()){!function(e){e.imageLoader.clear();e.drawer.clear();e.world.draw();e.raiseEvent("update-viewport",{})}(e);e._drawOverlays();e.navigator&&e.navigator.update(e.viewport);c[e.hash].forceRedraw=!1;h&&e.raiseEvent("animation")}if(c[e.hash].animating&&!h){e.raiseEvent("animation-finish");c[e.hash].mouseInside||p(e)}c[e.hash].animating=h}(e);e.isOpen()?e._updateRequestId=r(e,B):e._updateRequestId=!1}function D(e,t){return e?e+t:t}function M(){c[this.hash].lastZoomTime=m.now();c[this.hash].zoomFactor=this.zoomPerSecond;c[this.hash].zooming=!0;n(this)}function z(){c[this.hash].lastZoomTime=m.now();c[this.hash].zoomFactor=1/this.zoomPerSecond;c[this.hash].zooming=!0;n(this)}function H(){c[this.hash].zooming=!1}function n(e){m.requestAnimationFrame(m.delegate(e,t))}function t(){var e,t,i;if(c[this.hash].zooming&&this.viewport){t=(e=m.now())-c[this.hash].lastZoomTime;i=Math.pow(c[this.hash].zoomFactor,t/1e3);this.viewport.zoomBy(i);this.viewport.applyConstraints();c[this.hash].lastZoomTime=e;n(this)}}function F(){if(this.viewport){c[this.hash].zooming=!1;this.viewport.zoomBy(this.zoomPerClick/1);this.viewport.applyConstraints()}}function L(){if(this.viewport){c[this.hash].zooming=!1;this.viewport.zoomBy(1/this.zoomPerClick);this.viewport.applyConstraints()}}function A(){this.buttons.emulateEnter();this.buttons.emulateExit()}function N(){this.viewport&&this.viewport.goHome()}function W(){this.isFullPage()&&!m.isFullScreen()?this.setFullPage(!1):this.setFullScreen(!this.isFullPage());this.buttons&&this.buttons.emulateExit();this.fullPageButton.element.focus();this.viewport&&this.viewport.applyConstraints()}function V(){if(this.viewport){var e=this.viewport.getRotation();e=this.viewport.flipped?m.positiveModulo(e+this.rotationIncrement,360):m.positiveModulo(e-this.rotationIncrement,360);this.viewport.setRotation(e)}}function U(){if(this.viewport){var e=this.viewport.getRotation();e=this.viewport.flipped?m.positiveModulo(e-this.rotationIncrement,360):m.positiveModulo(e+this.rotationIncrement,360);this.viewport.setRotation(e)}}function j(){this.viewport.toggleFlip()}function G(){var e=this._sequenceIndex-1;this.navPrevNextWrap&&e<0&&(e+=this.tileSources.length);this.goToPage(e)}function $(){var e=this._sequenceIndex+1;this.navPrevNextWrap&&e>=this.tileSources.length&&(e=0);this.goToPage(e)}}(OpenSeadragon);!function(c){c.Navigator=function(i){var e,t,n=i.viewer,o=this;if(i.id){this.element=document.getElementById(i.id);i.controlOptions={anchor:c.ControlAnchor.NONE,attachToViewer:!1,autoFade:!1}}else{i.id="navigator-"+c.now();this.element=c.makeNeutralElement("div");i.controlOptions={anchor:c.ControlAnchor.TOP_RIGHT,attachToViewer:!0,autoFade:i.autoFade};if(i.position)if("BOTTOM_RIGHT"==i.position)i.controlOptions.anchor=c.ControlAnchor.BOTTOM_RIGHT;else if("BOTTOM_LEFT"==i.position)i.controlOptions.anchor=c.ControlAnchor.BOTTOM_LEFT;else if("TOP_RIGHT"==i.position)i.controlOptions.anchor=c.ControlAnchor.TOP_RIGHT;else if("TOP_LEFT"==i.position)i.controlOptions.anchor=c.ControlAnchor.TOP_LEFT;else if("ABSOLUTE"==i.position){i.controlOptions.anchor=c.ControlAnchor.ABSOLUTE;i.controlOptions.top=i.top;i.controlOptions.left=i.left;i.controlOptions.height=i.height;i.controlOptions.width=i.width}}this.element.id=i.id;this.element.className+=" navigator";(i=c.extend(!0,{sizeRatio:c.DEFAULT_SETTINGS.navigatorSizeRatio},i,{element:this.element,tabIndex:-1,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1,immediateRender:!0,blendTime:0,animationTime:0,autoResize:i.autoResize,minZoomImageRatio:1,background:i.background,opacity:i.opacity,borderColor:i.borderColor,displayRegionColor:i.displayRegionColor})).minPixelRatio=this.minPixelRatio=n.minPixelRatio;c.setElementTouchActionNone(this.element);this.borderWidth=2;this.fudge=new c.Point(1,1);this.totalBorderWidths=new c.Point(2*this.borderWidth,2*this.borderWidth).minus(this.fudge);i.controlOptions.anchor!=c.ControlAnchor.NONE&&function(e,t){e.margin="0px";e.border=t+"px solid "+i.borderColor;e.padding="0px";e.background=i.background;e.opacity=i.opacity;e.overflow="hidden"}(this.element.style,this.borderWidth);this.displayRegion=c.makeNeutralElement("div");this.displayRegion.id=this.element.id+"-displayregion";this.displayRegion.className="displayregion";!function(e,t){e.position="relative";e.top="0px";e.left="0px";e.fontSize="0px";e.overflow="hidden";e.border=t+"px solid "+i.displayRegionColor;e.margin="0px";e.padding="0px";e.background="transparent";e.float="left";e.cssFloat="left";e.styleFloat="left";e.zIndex=999999999;e.cursor="default"}(this.displayRegion.style,this.borderWidth);this.displayRegionContainer=c.makeNeutralElement("div");this.displayRegionContainer.id=this.element.id+"-displayregioncontainer";this.displayRegionContainer.className="displayregioncontainer";this.displayRegionContainer.style.width="100%";this.displayRegionContainer.style.height="100%";n.addControl(this.element,i.controlOptions);this._resizeWithViewer=i.controlOptions.anchor!=c.ControlAnchor.ABSOLUTE&&i.controlOptions.anchor!=c.ControlAnchor.NONE;if(i.width&&i.height){this.setWidth(i.width);this.setHeight(i.height)}else if(this._resizeWithViewer){e=c.getElementSize(n.element);this.element.style.height=Math.round(e.y*i.sizeRatio)+"px";this.element.style.width=Math.round(e.x*i.sizeRatio)+"px";this.oldViewerSize=e;t=c.getElementSize(this.element);this.elementArea=t.x*t.y}this.oldContainerSize=new c.Point(0,0);c.Viewer.apply(this,[i]);this.displayRegionContainer.appendChild(this.displayRegion);this.element.getElementsByTagName("div")[0].appendChild(this.displayRegionContainer);function r(e){u(o.displayRegionContainer,e);u(o.displayRegion,-e);o.viewport.setRotation(e)}if(i.navigatorRotate){r(i.viewer.viewport?i.viewer.viewport.getRotation():i.viewer.degrees||0);i.viewer.addHandler("rotate",function(e){r(e.degrees)})}this.innerTracker.destroy();this.innerTracker=new c.MouseTracker({element:this.element,dragHandler:c.delegate(this,a),clickHandler:c.delegate(this,s),releaseHandler:c.delegate(this,l),scrollHandler:c.delegate(this,h)});this.addHandler("reset-size",function(){o.viewport&&o.viewport.goHome(!0)});n.world.addHandler("item-index-change",function(t){window.setTimeout(function(){var e=o.world.getItemAt(t.previousIndex);o.world.setItemIndex(e,t.newIndex)},1)});n.world.addHandler("remove-item",function(e){var t=e.item;var i=o._getMatchingItem(t);i&&o.world.removeItem(i)});this.update(n.viewport)};c.extend(c.Navigator.prototype,c.EventSource.prototype,c.Viewer.prototype,{updateSize:function(){if(this.viewport){var e=new c.Point(0===this.container.clientWidth?1:this.container.clientWidth,0===this.container.clientHeight?1:this.container.clientHeight);if(!e.equals(this.oldContainerSize)){this.viewport.resize(e,!0);this.viewport.goHome(!0);this.oldContainerSize=e;this.drawer.clear();this.world.draw()}}},setWidth:function(e){this.width=e;this.element.style.width="number"==typeof e?e+"px":e;this._resizeWithViewer=!1},setHeight:function(e){this.height=e;this.element.style.height="number"==typeof e?e+"px":e;this._resizeWithViewer=!1},setFlip:function(e){this.viewport.setFlip(e);this.setDisplayTransform(this.viewer.viewport.getFlip()?"scale(-1,1)":"scale(1,1)");return this},setDisplayTransform:function(e){i(this.displayRegion,e);i(this.canvas,e);i(this.element,e)},update:function(e){var t,i,n,o,r,s;t=c.getElementSize(this.viewer.element);if(this._resizeWithViewer&&t.x&&t.y&&!t.equals(this.oldViewerSize)){this.oldViewerSize=t;if(this.maintainSizeRatio||!this.elementArea){i=t.x*this.sizeRatio;n=t.y*this.sizeRatio}else{i=Math.sqrt(this.elementArea*(t.x/t.y));n=this.elementArea/i}this.element.style.width=Math.round(i)+"px";this.element.style.height=Math.round(n)+"px";this.elementArea||(this.elementArea=i*n);this.updateSize()}if(e&&this.viewport){o=e.getBoundsNoRotate(!0);r=this.viewport.pixelFromPointNoRotate(o.getTopLeft(),!1);s=this.viewport.pixelFromPointNoRotate(o.getBottomRight(),!1).minus(this.totalBorderWidths);var a=this.displayRegion.style;a.display=this.world.getItemCount()?"block":"none";a.top=Math.round(r.y)+"px";a.left=Math.round(r.x)+"px";var l=Math.abs(r.x-s.x);var h=Math.abs(r.y-s.y);a.width=Math.round(Math.max(l,0))+"px";a.height=Math.round(Math.max(h,0))+"px"}},addTiledImage:function(e){var n=this;var o=e.originalTiledImage;delete e.original;var t=c.extend({},e,{success:function(e){var t=e.item;t._originalForNavigator=o;n._matchBounds(t,o,!0);function i(){n._matchBounds(t,o)}o.addHandler("bounds-change",i);o.addHandler("clip-change",i);o.addHandler("opacity-change",function(){n._matchOpacity(t,o)});o.addHandler("composite-operation-change",function(){n._matchCompositeOperation(t,o)})}});return c.Viewer.prototype.addTiledImage.apply(this,[t])},_getMatchingItem:function(e){var t=this.world.getItemCount();var i;for(var n=0;n=1/this.aspectRatio-1e-15&&(a=this.getNumTiles(e).y-1);return new d.Point(s,a)},getTileBounds:function(e,t,i,n){var o=this.dimensions.times(this.getLevelScale(e)),r=this.getTileWidth(e),s=this.getTileHeight(e),a=0===t?0:r*t-this.tileOverlap,l=0===i?0:s*i-this.tileOverlap,h=r+(0===t?1:2)*this.tileOverlap,c=s+(0===i?1:2)*this.tileOverlap,u=1/o.x;h=Math.min(h,o.x-a);c=Math.min(c,o.y-l);return n?new d.Rect(0,0,h,c):new d.Rect(a*u,l*u,h*u,c*u)},getImageInfo:function(n){var e,i,o,r,t,s,a,l=this;n&&-1<(a=(s=(t=n.split("/"))[t.length-1]).lastIndexOf("."))&&(t[t.length-1]=s.slice(0,a));i=function(e){"string"==typeof e&&(e=d.parseXml(e));var t=d.TileSource.determineType(l,e,n);if(t){void 0===(r=t.prototype.configure.apply(l,[e,n])).ajaxWithCredentials&&(r.ajaxWithCredentials=l.ajaxWithCredentials);o=new t(r);l.ready=!0;l.raiseEvent("ready",{tileSource:o})}else l.raiseEvent("open-failed",{message:"Unable to load TileSource",source:n})};if(n.match(/\.js$/)){e=n.split("/").pop().replace(".js","");d.jsonp({url:n,async:!1,callbackName:e,callback:i})}else d.makeAjaxRequest({url:n,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,success:function(e){var t=function(t){var e,i,n=t.responseText,o=t.status;{if(!t)throw new Error(d.getString("Errors.Security"));if(200!==t.status&&0!==t.status){o=t.status;e=404==o?"Not Found":t.statusText;throw new Error(d.getString("Errors.Status",o,e))}}if(n.match(/\s*<.*/))try{i=t.responseXML&&t.responseXML.documentElement?t.responseXML:d.parseXml(n)}catch(e){i=t.responseText}else if(n.match(/\s*[\{\[].*/))try{i=d.parseJSON(n)}catch(e){i=n}else i=n;return i}(e);i(t)},error:function(e,t){var i;try{i="HTTP "+e.status+" attempting to load TileSource"}catch(e){i=(void 0!==t&&t.toString?t.toString():"Unknown error")+" attempting to load TileSource"}l.raiseEvent("open-failed",{message:i,source:n})}})},supports:function(e,t){return!1},configure:function(e,t){throw new Error("Method not implemented.")},getTileUrl:function(e,t,i){throw new Error("Method not implemented.")},getTileAjaxHeaders:function(e,t,i){return{}},tileExists:function(e,t,i){var n=this.getNumTiles(e);return e>=this.minLevel&&e<=this.maxLevel&&0<=t&&0<=i&&tthis.maxLevel)return!1;if(!c||!c.length)return!0;for(h=c.length-1;0<=h;h--)if(!(e<(n=c[h]).minLevel||e>n.maxLevel)){o=this.getLevelScale(e);r=n.x*o;s=n.y*o;a=r+n.width*o;l=s+n.height*o;r=Math.floor(r/this._tileWidth);s=Math.floor(s/this._tileWidth);a=Math.ceil(a/this._tileWidth);l=Math.ceil(l/this._tileWidth);if(r<=t&&t=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t}return h.TileSource.prototype.getLevelScale.call(this,e)},getNumTiles:function(e){if(this.emulateLegacyImagePyramid){return this.getLevelScale(e)?new h.Point(1,1):new h.Point(0,0)}return h.TileSource.prototype.getNumTiles.call(this,e)},getTileAtPoint:function(e,t){return this.emulateLegacyImagePyramid?new h.Point(0,0):h.TileSource.prototype.getTileAtPoint.call(this,e,t)},getTileUrl:function(e,t,i){if(this.emulateLegacyImagePyramid){var n=null;0=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n}var o,r,s,a,l,h,c,u,d,p,g,m,v,f=Math.pow(.5,this.maxLevel-e),w=Math.ceil(this.width*f),y=Math.ceil(this.height*f);o=this.getTileWidth(e);r=this.getTileHeight(e);s=Math.ceil(o/f);a=Math.ceil(r/f);v=1===this.version?"native."+this.tileFormat:"default."+this.tileFormat;if(we.tileSize||parseInt(t.y,10)>e.tileSize;){t.x=Math.floor(t.x/2);t.y=Math.floor(t.y/2);e.imageSizes.push({x:t.x,y:t.y});e.gridSize.push(this._getGridSize(t.x,t.y,e.tileSize))}e.imageSizes.reverse();e.gridSize.reverse();e.minLevel=0;e.maxLevel=e.gridSize.length-1;OpenSeadragon.TileSource.apply(this,[e])};e.extend(e.ZoomifyTileSource.prototype,e.TileSource.prototype,{_getGridSize:function(e,t,i){return{x:Math.ceil(e/i),y:Math.ceil(t/i)}},_calculateAbsoluteTileNumber:function(e,t,i){var n=0;var o={};for(var r=0;r");return n.sort(function(e,t){return e.height-t.height})}(t.levels);if(0=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t},getNumTiles:function(e){return this.getLevelScale(e)?new l.Point(1,1):new l.Point(0,0)},getTileUrl:function(e,t,i){var n=null;0=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n}});function h(e,t){return t.levels}}(OpenSeadragon);!function(a){a.ImageTileSource=function(e){e=a.extend({buildPyramid:!0,crossOriginPolicy:!1,ajaxWithCredentials:!1,useCanvas:!0},e);a.TileSource.apply(this,[e])};a.extend(a.ImageTileSource.prototype,a.TileSource.prototype,{supports:function(e,t){return e.type&&"image"===e.type},configure:function(e,t){return e},getImageInfo:function(e){var t=this._image=new Image;var i=this;this.crossOriginPolicy&&(t.crossOrigin=this.crossOriginPolicy);this.ajaxWithCredentials&&(t.useCredentials=this.ajaxWithCredentials);a.addEvent(t,"load",function(){i.width=Object.prototype.hasOwnProperty.call(t,"naturalWidth")?t.naturalWidth:t.width;i.height=Object.prototype.hasOwnProperty.call(t,"naturalHeight")?t.naturalHeight:t.height;i.aspectRatio=i.width/i.height;i.dimensions=new a.Point(i.width,i.height);i._tileWidth=i.width;i._tileHeight=i.height;i.tileOverlap=0;i.minLevel=0;i.levels=i._buildLevels();i.maxLevel=i.levels.length-1;i.ready=!0;i.raiseEvent("ready",{tileSource:i})});a.addEvent(t,"error",function(){i.raiseEvent("open-failed",{message:"Error loading image at "+e,source:e})});t.src=e},getLevelScale:function(e){var t=NaN;e>=this.minLevel&&e<=this.maxLevel&&(t=this.levels[e].width/this.levels[this.maxLevel].width);return t},getNumTiles:function(e){return this.getLevelScale(e)?new a.Point(1,1):new a.Point(0,0)},getTileUrl:function(e,t,i){var n=null;e>=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].url);return n},getContext2D:function(e,t,i){var n=null;e>=this.minLevel&&e<=this.maxLevel&&(n=this.levels[e].context2D);return n},_buildLevels:function(){var e=[{url:this._image.src,width:Object.prototype.hasOwnProperty.call(this._image,"naturalWidth")?this._image.naturalWidth:this._image.width,height:Object.prototype.hasOwnProperty.call(this._image,"naturalHeight")?this._image.naturalHeight:this._image.height}];if(!this.buildPyramid||!a.supportsCanvas||!this.useCanvas){delete this._image;return e}var t=Object.prototype.hasOwnProperty.call(this._image,"naturalWidth")?this._image.naturalWidth:this._image.width;var i=Object.prototype.hasOwnProperty.call(this._image,"naturalHeight")?this._image.naturalHeight:this._image.height;var n=document.createElement("canvas");var o=n.getContext("2d");n.width=t;n.height=i;o.drawImage(this._image,0,0,t,i);e[0].context2D=o;delete this._image;if(a.isCanvasTainted(n))return e;for(;2<=t&&2<=i;){t=Math.floor(t/2);i=Math.floor(i/2);var r=document.createElement("canvas");var s=r.getContext("2d");r.width=t;r.height=i;s.drawImage(n,0,0,t,i);e.splice(0,0,{context2D:s,width:t,height:i});n=r;o=s}return e}})}(OpenSeadragon);!function(o){o.TileSourceCollection=function(e,t,i,n){o.console.error("TileSourceCollection is deprecated; use World instead")}}(OpenSeadragon);!function(o){o.ButtonState={REST:0,GROUP:1,HOVER:2,DOWN:3};o.Button=function(e){var t=this;o.EventSource.call(this);o.extend(!0,this,{tooltip:null,srcRest:null,srcGroup:null,srcHover:null,srcDown:null,clickTimeThreshold:o.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:o.DEFAULT_SETTINGS.clickDistThreshold,fadeDelay:0,fadeLength:2e3,onPress:null,onRelease:null,onClick:null,onEnter:null,onExit:null,onFocus:null,onBlur:null},e);this.element=e.element||o.makeNeutralElement("div");if(!e.element){this.imgRest=o.makeTransparentImage(this.srcRest);this.imgGroup=o.makeTransparentImage(this.srcGroup);this.imgHover=o.makeTransparentImage(this.srcHover);this.imgDown=o.makeTransparentImage(this.srcDown);this.imgRest.alt=this.imgGroup.alt=this.imgHover.alt=this.imgDown.alt=this.tooltip;this.element.style.position="relative";o.setElementTouchActionNone(this.element);this.imgGroup.style.position=this.imgHover.style.position=this.imgDown.style.position="absolute";this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="0px";this.imgGroup.style.left=this.imgHover.style.left=this.imgDown.style.left="0px";this.imgHover.style.visibility=this.imgDown.style.visibility="hidden";o.Browser.vendor==o.BROWSERS.FIREFOX&&o.Browser.version<3&&(this.imgGroup.style.top=this.imgHover.style.top=this.imgDown.style.top="");this.element.appendChild(this.imgRest);this.element.appendChild(this.imgGroup);this.element.appendChild(this.imgHover);this.element.appendChild(this.imgDown)}this.addHandler("press",this.onPress);this.addHandler("release",this.onRelease);this.addHandler("click",this.onClick);this.addHandler("enter",this.onEnter);this.addHandler("exit",this.onExit);this.addHandler("focus",this.onFocus);this.addHandler("blur",this.onBlur);this.currentState=o.ButtonState.GROUP;this.fadeBeginTime=null;this.shouldFade=!1;this.element.style.display="inline-block";this.element.style.position="relative";this.element.title=this.tooltip;this.tracker=new o.MouseTracker({element:this.element,clickTimeThreshold:this.clickTimeThreshold,clickDistThreshold:this.clickDistThreshold,enterHandler:function(e){if(e.insideElementPressed){i(t,o.ButtonState.DOWN);t.raiseEvent("enter",{originalEvent:e.originalEvent})}else e.buttonDownAny||i(t,o.ButtonState.HOVER)},focusHandler:function(e){this.enterHandler(e);t.raiseEvent("focus",{originalEvent:e.originalEvent})},exitHandler:function(e){n(t,o.ButtonState.GROUP);e.insideElementPressed&&t.raiseEvent("exit",{originalEvent:e.originalEvent})},blurHandler:function(e){this.exitHandler(e);t.raiseEvent("blur",{originalEvent:e.originalEvent})},pressHandler:function(e){i(t,o.ButtonState.DOWN);t.raiseEvent("press",{originalEvent:e.originalEvent})},releaseHandler:function(e){if(e.insideElementPressed&&e.insideElementReleased){n(t,o.ButtonState.HOVER);t.raiseEvent("release",{originalEvent:e.originalEvent})}else e.insideElementPressed?n(t,o.ButtonState.GROUP):i(t,o.ButtonState.HOVER)},clickHandler:function(e){e.quick&&t.raiseEvent("click",{originalEvent:e.originalEvent})},keyHandler:function(e){if(13!==e.keyCode)return!0;t.raiseEvent("click",{originalEvent:e.originalEvent});t.raiseEvent("release",{originalEvent:e.originalEvent});return!1}});n(this,o.ButtonState.REST)};o.extend(o.Button.prototype,o.EventSource.prototype,{notifyGroupEnter:function(){i(this,o.ButtonState.GROUP)},notifyGroupExit:function(){n(this,o.ButtonState.REST)},disable:function(){this.notifyGroupExit();this.element.disabled=!0;o.setElementOpacity(this.element,.2,!0)},enable:function(){this.element.disabled=!1;o.setElementOpacity(this.element,1,!0);this.notifyGroupEnter()}});function r(e){o.requestAnimationFrame(function(){!function(e){var t,i,n;if(e.shouldFade){t=o.now();i=t-e.fadeBeginTime;n=1-i/e.fadeLength;n=Math.min(1,n);n=Math.max(0,n);e.imgGroup&&o.setElementOpacity(e.imgGroup,n,!0);0=o.ButtonState.GROUP&&e.currentState==o.ButtonState.REST){!function(e){e.shouldFade=!1;e.imgGroup&&o.setElementOpacity(e.imgGroup,1,!0)}(e);e.currentState=o.ButtonState.GROUP}if(t>=o.ButtonState.HOVER&&e.currentState==o.ButtonState.GROUP){e.imgHover&&(e.imgHover.style.visibility="");e.currentState=o.ButtonState.HOVER}if(t>=o.ButtonState.DOWN&&e.currentState==o.ButtonState.HOVER){e.imgDown&&(e.imgDown.style.visibility="");e.currentState=o.ButtonState.DOWN}}}function n(e,t){if(!e.element.disabled){if(t<=o.ButtonState.HOVER&&e.currentState==o.ButtonState.DOWN){e.imgDown&&(e.imgDown.style.visibility="hidden");e.currentState=o.ButtonState.HOVER}if(t<=o.ButtonState.GROUP&&e.currentState==o.ButtonState.HOVER){e.imgHover&&(e.imgHover.style.visibility="hidden");e.currentState=o.ButtonState.GROUP}if(t<=o.ButtonState.REST&&e.currentState==o.ButtonState.GROUP){!function(e){e.shouldFade=!0;e.fadeBeginTime=o.now()+e.fadeDelay;window.setTimeout(function(){r(e)},e.fadeDelay)}(e);e.currentState=o.ButtonState.REST}}}}(OpenSeadragon);!function(o){o.ButtonGroup=function(e){o.extend(!0,this,{buttons:[],clickTimeThreshold:o.DEFAULT_SETTINGS.clickTimeThreshold,clickDistThreshold:o.DEFAULT_SETTINGS.clickDistThreshold,labelText:""},e);var t,i=this.buttons.concat([]),n=this;this.element=e.element||o.makeNeutralElement("div");if(!e.group){this.element.style.display="inline-block";for(t=0;tT&&(T=P.x);P.yS&&(S=P.y)}return new R.Rect(y,x,T-y,S-x)},_getSegments:function(){var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();return[[e,t],[t,n],[n,i],[i,e]]},rotate:function(e,t){if(0===(e=R.positiveModulo(e,360)))return this.clone();t=t||this.getCenter();var i=this.getTopLeft().rotate(e,t);var n=this.getTopRight().rotate(e,t).minus(i);n=n.apply(function(e){return Math.abs(e)<1e-15?0:e});var o=Math.atan(n.y/n.x);n.x<0?o+=Math.PI:n.y<0&&(o+=2*Math.PI);return new R.Rect(i.x,i.y,this.width,this.height,o/Math.PI*180)},getBoundingBox:function(){if(0===this.degrees)return this.clone();var e=this.getTopLeft();var t=this.getTopRight();var i=this.getBottomLeft();var n=this.getBottomRight();var o=Math.min(e.x,t.x,i.x,n.x);var r=Math.max(e.x,t.x,i.x,n.x);var s=Math.min(e.y,t.y,i.y,n.y);var a=Math.max(e.y,t.y,i.y,n.y);return new R.Rect(o,s,r-o,a-s)},getIntegerBoundingBox:function(){var e=this.getBoundingBox();var t=Math.floor(e.x);var i=Math.floor(e.y);var n=Math.ceil(e.width+e.x-t);var o=Math.ceil(e.height+e.y-i);return new R.Rect(t,i,n,o)},containsPoint:function(e,t){t=t||0;var i=this.getTopLeft();var n=this.getTopRight();var o=this.getBottomLeft();var r=n.minus(i);var s=o.minus(i);return(e.x-i.x)*r.x+(e.y-i.y)*r.y>=-t&&(e.x-n.x)*r.x+(e.y-n.y)*r.y<=t&&(e.x-i.x)*s.x+(e.y-i.y)*s.y>=-t&&(e.x-o.x)*s.x+(e.y-o.y)*s.y<=t},toString:function(){return"["+Math.round(100*this.x)/100+", "+Math.round(100*this.y)/100+", "+Math.round(100*this.width)/100+"x"+Math.round(100*this.height)/100+", "+Math.round(100*this.degrees)/100+"deg]"}}}(OpenSeadragon);!function(d){var s={};d.ReferenceStrip=function(e){var t,i,n,r=e.viewer,o=d.getElementSize(r.element);if(!e.id){e.id="referencestrip-"+d.now();this.element=d.makeNeutralElement("div");this.element.id=e.id;this.element.className="referencestrip"}e=d.extend(!0,{sizeRatio:d.DEFAULT_SETTINGS.referenceStripSizeRatio,position:d.DEFAULT_SETTINGS.referenceStripPosition,scroll:d.DEFAULT_SETTINGS.referenceStripScroll,clickTimeThreshold:d.DEFAULT_SETTINGS.clickTimeThreshold},e,{element:this.element,showNavigator:!1,mouseNavEnabled:!1,showNavigationControl:!1,showSequenceControl:!1});d.extend(this,e);s[this.id]={animating:!1};this.minPixelRatio=this.viewer.minPixelRatio;(i=this.element.style).marginTop="0px";i.marginRight="0px";i.marginBottom="0px";i.marginLeft="0px";i.left="0px";i.bottom="0px";i.border="0px";i.background="#000";i.position="relative";d.setElementTouchActionNone(this.element);d.setElementOpacity(this.element,.8);this.viewer=r;this.innerTracker=new d.MouseTracker({element:this.element,dragHandler:d.delegate(this,a),scrollHandler:d.delegate(this,l),enterHandler:d.delegate(this,c),exitHandler:d.delegate(this,u),keyDownHandler:d.delegate(this,p),keyHandler:d.delegate(this,g)});if(e.width&&e.height){this.element.style.width=e.width+"px";this.element.style.height=e.height+"px";r.addControl(this.element,{anchor:d.ControlAnchor.BOTTOM_LEFT})}else if("horizontal"==e.scroll){this.element.style.width=o.x*e.sizeRatio*r.tileSources.length+12*r.tileSources.length+"px";this.element.style.height=o.y*e.sizeRatio+"px";r.addControl(this.element,{anchor:d.ControlAnchor.BOTTOM_LEFT})}else{this.element.style.height=o.y*e.sizeRatio*r.tileSources.length+12*r.tileSources.length+"px";this.element.style.width=o.x*e.sizeRatio+"px";r.addControl(this.element,{anchor:d.ControlAnchor.TOP_LEFT})}this.panelWidth=o.x*this.sizeRatio+8;this.panelHeight=o.y*this.sizeRatio+8;this.panels=[];this.miniViewers={};for(n=0;ns+n.x-this.panelWidth){t=Math.min(t,o-n.x);this.element.style.marginLeft=-t+"px";h(this,n.x,-t)}else if(ta+n.y-this.panelHeight){t=Math.min(t,r-n.y);this.element.style.marginTop=-t+"px";h(this,n.y,-t)}else if(t-(n-r.x)){this.element.style.marginLeft=t+2*e.delta.x+"px";h(this,r.x,t+2*e.delta.x)}}else if(-e.delta.x<0&&t<0){this.element.style.marginLeft=t+2*e.delta.x+"px";h(this,r.x,t+2*e.delta.x)}}else if(0<-e.delta.y){if(i>-(o-r.y)){this.element.style.marginTop=i+2*e.delta.y+"px";h(this,r.y,i+2*e.delta.y)}}else if(-e.delta.y<0&&i<0){this.element.style.marginTop=i+2*e.delta.y+"px";h(this,r.y,i+2*e.delta.y)}return!1}function l(e){var t=Number(this.element.style.marginLeft.replace("px","")),i=Number(this.element.style.marginTop.replace("px","")),n=Number(this.element.style.width.replace("px","")),o=Number(this.element.style.height.replace("px","")),r=d.getElementSize(this.viewer.canvas);if(this.element)if("horizontal"==this.scroll){if(0-(n-r.x)){this.element.style.marginLeft=t-60*e.scroll+"px";h(this,r.x,t-60*e.scroll)}}else if(e.scroll<0&&t<0){this.element.style.marginLeft=t-60*e.scroll+"px";h(this,r.x,t-60*e.scroll)}}else if(e.scroll<0){if(i>r.y-o){this.element.style.marginTop=i+60*e.scroll+"px";h(this,r.y,i+60*e.scroll)}}else if(0=this.target.time?t:e+(t-e)*(n=this.springStiffness,o=(this.current.time-this.start.time)/(this.target.time-this.start.time),(1-Math.exp(n*-o))/(1-Math.exp(-n)));var n,o;var r=this.current.value;this._exponential?this.current.value=Math.exp(i):this.current.value=i;return r!=this.current.value},isAtTargetValue:function(){return this.current.value===this.target.value}}}(OpenSeadragon);!function(t){function n(e){t.extend(!0,this,{timeout:t.DEFAULT_SETTINGS.timeout,jobId:null},e);this.image=null}n.prototype={errorMsg:null,start:function(){var r=this;var e=this.abort;this.image=new Image;this.image.onload=function(){r.finish(!0)};this.image.onabort=this.image.onerror=function(){r.errorMsg="Image load aborted";r.finish(!1)};this.jobId=window.setTimeout(function(){r.errorMsg="Image load exceeded timeout ("+r.timeout+" ms)";r.finish(!1)},this.timeout);if(this.loadWithAjax){this.request=t.makeAjaxRequest({url:this.src,withCredentials:this.ajaxWithCredentials,headers:this.ajaxHeaders,responseType:"arraybuffer",success:function(t){var i;try{i=new window.Blob([t.response])}catch(e){var n=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder||window.MSBlobBuilder;if("TypeError"===e.name&&n){var o=new n;o.append(t.response);i=o.getBlob()}}if(0===i.size){r.errorMsg="Empty image response.";r.finish(!1)}var e=(window.URL||window.webkitURL).createObjectURL(i);r.image.src=e},error:function(e){r.errorMsg="Image load aborted - XHR error";r.finish(!1)}});this.abort=function(){r.request.abort();"function"==typeof e&&e()}}else{!1!==this.crossOriginPolicy&&(this.image.crossOrigin=this.crossOriginPolicy);this.image.src=this.src}},finish:function(e){this.image.onload=this.image.onerror=this.image.onabort=null;e||(this.image=null);this.jobId&&window.clearTimeout(this.jobId);this.callback(this)}};t.ImageLoader=function(e){t.extend(!0,this,{jobLimit:t.DEFAULT_SETTINGS.imageLoaderLimit,timeout:t.DEFAULT_SETTINGS.timeout,jobQueue:[],jobsInProgress:0},e)};t.ImageLoader.prototype={addJob:function(t){var i=this,e=new n({src:t.src,loadWithAjax:t.loadWithAjax,ajaxHeaders:t.loadWithAjax?t.ajaxHeaders:null,crossOriginPolicy:t.crossOriginPolicy,ajaxWithCredentials:t.ajaxWithCredentials,callback:function(e){!function(e,t,i){e.jobsInProgress--;if((!e.jobLimit||e.jobsInProgressthis.canvas.width&&(r.width=this.canvas.width-r.x);if(r.y<0){r.height+=r.y;r.y=0}r.y+r.height>this.canvas.height&&(r.height=this.canvas.height-r.y);this.context.drawImage(this.sketchCanvas,r.x,r.y,r.width,r.height,r.x,r.y,r.width,r.height)}else{t=o.scale||1;var s=(i=o.translate)instanceof u.Point?i:new u.Point(0,0);var a=0;var l=0;if(i){var h=this.sketchCanvas.width-this.canvas.width;var c=this.sketchCanvas.height-this.canvas.height;a=Math.round(h/2);l=Math.round(c/2)}this.context.drawImage(this.sketchCanvas,s.x-a*t,s.y-l*t,(this.canvas.width+2*a)*t,(this.canvas.height+2*l)*t,-a,-l,this.canvas.width+2*a,this.canvas.height+2*l)}this.context.restore()}},drawDebugInfo:function(e,t,i,n){if(this.useCanvas){var o=this.viewer.world.getIndexOfItem(n)%this.debugGridColor.length;var r=this.context;r.save();r.lineWidth=2*u.pixelDensityRatio;r.font="small-caps bold "+13*u.pixelDensityRatio+"px arial";r.strokeStyle=this.debugGridColor[o];r.fillStyle=this.debugGridColor[o];0!==this.viewport.degrees&&this._offsetForRotation({degrees:this.viewport.degrees});n.getRotation(!0)%360!=0&&this._offsetForRotation({degrees:n.getRotation(!0),point:n.viewport.pixelFromPointNoRotate(n._getRotationPoint(!0),!0)});0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip();r.strokeRect(e.position.x*u.pixelDensityRatio,e.position.y*u.pixelDensityRatio,e.size.x*u.pixelDensityRatio,e.size.y*u.pixelDensityRatio);var s=(e.position.x+e.size.x/2)*u.pixelDensityRatio;var a=(e.position.y+e.size.y/2)*u.pixelDensityRatio;r.translate(s,a);r.rotate(Math.PI/180*-this.viewport.degrees);r.translate(-s,-a);if(0===e.x&&0===e.y){r.fillText("Zoom: "+this.viewport.getZoom(),e.position.x*u.pixelDensityRatio,(e.position.y-30)*u.pixelDensityRatio);r.fillText("Pan: "+this.viewport.getBounds().toString(),e.position.x*u.pixelDensityRatio,(e.position.y-20)*u.pixelDensityRatio)}r.fillText("Level: "+e.level,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+20)*u.pixelDensityRatio);r.fillText("Column: "+e.x,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+30)*u.pixelDensityRatio);r.fillText("Row: "+e.y,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+40)*u.pixelDensityRatio);r.fillText("Order: "+i+" of "+t,(e.position.x+10)*u.pixelDensityRatio,(e.position.y+50)*u.pixelDensityRatio);r.fillText("Size: "+e.size.toString(),(e.position.x+10)*u.pixelDensityRatio,(e.position.y+60)*u.pixelDensityRatio);r.fillText("Position: "+e.position.toString(),(e.position.x+10)*u.pixelDensityRatio,(e.position.y+70)*u.pixelDensityRatio);0!==this.viewport.degrees&&this._restoreRotationChanges();n.getRotation(!0)%360!=0&&this._restoreRotationChanges();0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip();r.restore()}},debugRect:function(e){if(this.useCanvas){var t=this.context;t.save();t.lineWidth=2*u.pixelDensityRatio;t.strokeStyle=this.debugGridColor[0];t.fillStyle=this.debugGridColor[0];t.strokeRect(e.x*u.pixelDensityRatio,e.y*u.pixelDensityRatio,e.width*u.pixelDensityRatio,e.height*u.pixelDensityRatio);t.restore()}},setImageSmoothingEnabled:function(e){if(this.useCanvas){this._imageSmoothingEnabled=e;this._updateImageSmoothingEnabled(this.context);this.viewer.forceRedraw()}},_updateImageSmoothingEnabled:function(e){e.msImageSmoothingEnabled=this._imageSmoothingEnabled;e.imageSmoothingEnabled=this._imageSmoothingEnabled},getCanvasSize:function(e){var t=this._getContext(e).canvas;return new u.Point(t.width,t.height)},getCanvasCenter:function(){return new u.Point(this.canvas.width/2,this.canvas.height/2)},_offsetForRotation:function(e){var t=e.point?e.point.times(u.pixelDensityRatio):this.getCanvasCenter();var i=this._getContext(e.useSketch);i.save();i.translate(t.x,t.y);if(this.viewer.viewport.flipped){i.rotate(Math.PI/180*-e.degrees);i.scale(-1,1)}else i.rotate(Math.PI/180*e.degrees);i.translate(-t.x,-t.y)},_flip:function(e){var t=(e=e||{}).point?e.point.times(u.pixelDensityRatio):this.getCanvasCenter();var i=this._getContext(e.useSketch);i.translate(t.x,0);i.scale(-1,1);i.translate(-t.x,0)},_restoreRotationChanges:function(e){this._getContext(e).restore()},_calculateCanvasSize:function(){var e=u.pixelDensityRatio;var t=this.viewport.getContainerSize();return{x:Math.round(t.x*e),y:Math.round(t.y*e)}},_calculateSketchCanvasSize:function(){var e=this._calculateCanvasSize();if(0===this.viewport.getRotation())return e;var t=Math.ceil(Math.sqrt(e.x*e.x+e.y*e.y));return{x:t,y:t}}}}(OpenSeadragon);!function(p){p.Viewport=function(e){var t=arguments;t.length&&t[0]instanceof p.Point&&(e={containerSize:t[0],contentSize:t[1],config:t[2]});if(e.config){p.extend(!0,e,e.config);delete e.config}this._margins=p.extend({left:0,top:0,right:0,bottom:0},e.margins||{});delete e.margins;p.extend(!0,this,{containerSize:null,contentSize:null,zoomPoint:null,viewer:null,springStiffness:p.DEFAULT_SETTINGS.springStiffness,animationTime:p.DEFAULT_SETTINGS.animationTime,minZoomImageRatio:p.DEFAULT_SETTINGS.minZoomImageRatio,maxZoomPixelRatio:p.DEFAULT_SETTINGS.maxZoomPixelRatio,visibilityRatio:p.DEFAULT_SETTINGS.visibilityRatio,wrapHorizontal:p.DEFAULT_SETTINGS.wrapHorizontal,wrapVertical:p.DEFAULT_SETTINGS.wrapVertical,defaultZoomLevel:p.DEFAULT_SETTINGS.defaultZoomLevel,minZoomLevel:p.DEFAULT_SETTINGS.minZoomLevel,maxZoomLevel:p.DEFAULT_SETTINGS.maxZoomLevel,degrees:p.DEFAULT_SETTINGS.degrees,flipped:p.DEFAULT_SETTINGS.flipped,homeFillsViewer:p.DEFAULT_SETTINGS.homeFillsViewer},e);this._updateContainerInnerSize();this.centerSpringX=new p.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.centerSpringY=new p.Spring({initial:0,springStiffness:this.springStiffness,animationTime:this.animationTime});this.zoomSpring=new p.Spring({exponential:!0,initial:1,springStiffness:this.springStiffness,animationTime:this.animationTime});this._oldCenterX=this.centerSpringX.current.value;this._oldCenterY=this.centerSpringY.current.value;this._oldZoom=this.zoomSpring.current.value;this._setContentBounds(new p.Rect(0,0,1,1),1);this.goHome(!0);this.update()};p.Viewport.prototype={resetContentSize:function(e){p.console.assert(e,"[Viewport.resetContentSize] contentSize is required");p.console.assert(e instanceof p.Point,"[Viewport.resetContentSize] contentSize must be an OpenSeadragon.Point");p.console.assert(0this._contentBoundsNoRotate.width?t.x+=(r+s)/2:s<0?t.x+=s:0this._contentBoundsNoRotate.height?t.y+=(c+u)/2:u<0?t.y+=u:0=o?s.height=s.width/o:s.width=s.height*o;s.x=r.x-s.width/2;s.y=r.y-s.height/2;var a=1/s.width;if(n){var l=s.getAspectRatio();var h=this._applyZoomConstraints(a);if(a!==h){a=h;s.width=1/a;s.x=r.x-s.width/2;s.height=s.width/l;s.y=r.y-s.height/2}r=(s=this._applyBoundaryConstraints(s)).getCenter();this._raiseConstraintsEvent(i)}if(i){this.panTo(r,!0);return this.zoomTo(a,null,!0)}this.panTo(this.getCenter(!0),!0);this.zoomTo(this.getZoom(!0),null,!0);var c=this.getBounds();var u=this.getZoom();if(0===u||Math.abs(a/u-1)<1e-8){this.zoomTo(a,!0);return this.panTo(r,i)}var d=(s=s.rotate(-this.getRotation())).getTopLeft().times(a).minus(c.getTopLeft().times(u)).divide(a-u);return this.zoomTo(a,d,i)},fitBounds:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!1})},fitBoundsWithConstraints:function(e,t){return this._fitBounds(e,{immediately:t,constraints:!0})},fitVertically:function(e){var t=new p.Rect(this._contentBounds.x+this._contentBounds.width/2,this._contentBounds.y,0,this._contentBounds.height);return this.fitBounds(t,e)},fitHorizontally:function(e){var t=new p.Rect(this._contentBounds.x,this._contentBounds.y+this._contentBounds.height/2,this._contentBounds.width,0);return this.fitBounds(t,e)},getConstrainedBounds:function(e){var t;t=this.getBounds(e);return this._applyBoundaryConstraints(t)},panBy:function(e,t){var i=new p.Point(this.centerSpringX.target.value,this.centerSpringY.target.value);return this.panTo(i.plus(e),t)},panTo:function(e,t){if(t){this.centerSpringX.resetTo(e.x);this.centerSpringY.resetTo(e.y)}else{this.centerSpringX.springTo(e.x);this.centerSpringY.springTo(e.y)}this.viewer&&this.viewer.raiseEvent("pan",{center:e,immediately:t});return this},zoomBy:function(e,t,i){return this.zoomTo(this.zoomSpring.target.value*e,t,i)},zoomTo:function(e,t,i){var n=this;this.zoomPoint=t instanceof p.Point&&!isNaN(t.x)&&!isNaN(t.y)?t:null;i?this._adjustCenterSpringsForZoomPoint(function(){n.zoomSpring.resetTo(e)}):this.zoomSpring.springTo(e);this.viewer&&this.viewer.raiseEvent("zoom",{zoom:e,refPoint:t,immediately:i});return this},setRotation:function(e){if(!this.viewer||!this.viewer.drawer.canRotate())return this;this.degrees=p.positiveModulo(e,360);this._setContentBounds(this.viewer.world.getHomeBounds(),this.viewer.world.getContentFactor());this.viewer.forceRedraw();this.viewer.raiseEvent("rotate",{degrees:e});return this},getRotation:function(){return this.degrees},resize:function(e,t){var i,n=this.getBoundsNoRotate(),o=n;this.containerSize.x=e.x;this.containerSize.y=e.y;this._updateContainerInnerSize();if(t){i=e.x/this.containerSize.x;o.width=n.width*i;o.height=o.width/this.getAspectRatio()}this.viewer&&this.viewer.raiseEvent("resize",{newContainerSize:e,maintain:t});return this.fitBounds(o,!0)},_updateContainerInnerSize:function(){this._containerInnerSize=new p.Point(Math.max(1,this.containerSize.x-(this._margins.left+this._margins.right)),Math.max(1,this.containerSize.y-(this._margins.top+this._margins.bottom)))},update:function(){var e=this;this._adjustCenterSpringsForZoomPoint(function(){e.zoomSpring.update()});this.centerSpringX.update();this.centerSpringY.update();var t=this.centerSpringX.current.value!==this._oldCenterX||this.centerSpringY.current.value!==this._oldCenterY||this.zoomSpring.current.value!==this._oldZoom;this._oldCenterX=this.centerSpringX.current.value;this._oldCenterY=this.centerSpringY.current.value;this._oldZoom=this.zoomSpring.current.value;return t},_adjustCenterSpringsForZoomPoint:function(e){if(this.zoomPoint){var t=this.pixelFromPoint(this.zoomPoint,!0);e();var i=this.pixelFromPoint(this.zoomPoint,!0).minus(t);var n=this.deltaPointsFromPixels(i,!0);this.centerSpringX.shiftBy(n.x);this.centerSpringY.shiftBy(n.y);this.zoomSpring.isAtTargetValue()&&(this.zoomPoint=null)}else e()},deltaPixelsFromPointsNoRotate:function(e,t){return e.times(this._containerInnerSize.x*this.getZoom(t))},deltaPixelsFromPoints:function(e,t){return this.deltaPixelsFromPointsNoRotate(e.rotate(this.getRotation()),t)},deltaPointsFromPixelsNoRotate:function(e,t){return e.divide(this._containerInnerSize.x*this.getZoom(t))},deltaPointsFromPixels:function(e,t){return this.deltaPointsFromPixelsNoRotate(e,t).rotate(-this.getRotation())},pixelFromPointNoRotate:function(e,t){return this._pixelFromPointNoRotate(e,this.getBoundsNoRotate(t))},pixelFromPoint:function(e,t){return this._pixelFromPoint(e,this.getBoundsNoRotate(t))},_pixelFromPointNoRotate:function(e,t){return e.minus(t.getTopLeft()).times(this._containerInnerSize.x/t.width).plus(new p.Point(this._margins.left,this._margins.top))},_pixelFromPoint:function(e,t){return this._pixelFromPointNoRotate(e.rotate(this.getRotation(),this.getCenter(!0)),t)},pointFromPixelNoRotate:function(e,t){var i=this.getBoundsNoRotate(t);return e.minus(new p.Point(this._margins.left,this._margins.top)).divide(this._containerInnerSize.x/i.width).plus(i.getTopLeft())},pointFromPixel:function(e,t){return this.pointFromPixelNoRotate(e,t).rotate(-this.getRotation(),this.getCenter(!0))},_viewportToImageDelta:function(e,t){var i=this._contentBoundsNoRotate.width;return new p.Point(e*this._contentSizeNoRotate.x/i,t*this._contentSizeNoRotate.x/i)},viewportToImageCoordinates:function(e,t){if(e instanceof p.Point)return this.viewportToImageCoordinates(e.x,e.y);if(this.viewer){var i=this.viewer.world.getItemCount();if(1o){r=this._clip.x/this._clip.height*e.height;s=this._clip.y/this._clip.height*e.height}else{r=this._clip.x/this._clip.width*e.width;s=this._clip.y/this._clip.width*e.width}}if(e.getAspectRatio()>o){var h=e.height/l;var c=0;n.isHorizontallyCentered?c=(e.width-e.height*o)/2:n.isRight&&(c=e.width-e.height*o);this.setPosition(new y.Point(e.x-r+c,e.y-s),i);this.setHeight(h,i)}else{var u=e.width/a;var d=0;n.isVerticallyCentered?d=(e.height-e.width/o)/2:n.isBottom&&(d=e.height-e.width/o);this.setPosition(new y.Point(e.x-r,e.y-s+d),i);this.setWidth(u,i)}},getClip:function(){return this._clip?this._clip.clone():null},setClip:function(e){y.console.assert(!e||e instanceof y.Rect,"[TiledImage.setClip] newClip must be an OpenSeadragon.Rect or null");e instanceof y.Rect?this._clip=e.clone():this._clip=null;this._needsDraw=!0;this.raiseEvent("clip-change")},getOpacity:function(){return this.opacity},setOpacity:function(e){if(e!==this.opacity){this.opacity=e;this._needsDraw=!0;this.raiseEvent("opacity-change",{opacity:this.opacity})}},getPreload:function(){return this._preload},setPreload:function(e){this._preload=!!e;this._needsDraw=!0},getRotation:function(e){return e?this._degreesSpring.current.value:this._degreesSpring.target.value},setRotation:function(e,t){if(this._degreesSpring.target.value!==e||!this._degreesSpring.isAtTargetValue()){t?this._degreesSpring.resetTo(e):this._degreesSpring.springTo(e);this._needsDraw=!0;this._raiseBoundsChange()}},_getRotationPoint:function(e){return this.getBoundsNoRotate(e).getCenter()},getCompositeOperation:function(){return this.compositeOperation},setCompositeOperation:function(e){if(e!==this.compositeOperation){this.compositeOperation=e;this._needsDraw=!0;this.raiseEvent("composite-operation-change",{compositeOperation:this.compositeOperation})}},_setScale:function(e,t){var i=this._scaleSpring.target.value===e;if(t){if(i&&this._scaleSpring.current.value===e)return;this._scaleSpring.resetTo(e);this._updateForScale();this._needsDraw=!0}else{if(i)return;this._scaleSpring.springTo(e);this._updateForScale();this._needsDraw=!0}i||this._raiseBoundsChange()},_updateForScale:function(){this._worldWidthTarget=this._scaleSpring.target.value;this._worldHeightTarget=this.normHeight*this._scaleSpring.target.value;this._worldWidthCurrent=this._scaleSpring.current.value;this._worldHeightCurrent=this.normHeight*this._scaleSpring.current.value},_raiseBoundsChange:function(){this.raiseEvent("bounds-change")},_isBottomItem:function(){return this.viewer.world.getItemAt(0)===this},_getLevelsInterval:function(){var e=Math.max(this.source.minLevel,Math.floor(Math.log(this.minZoomImageRatio)/Math.log(2)));var t=this.viewport.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(0),!0).x*this._scaleSpring.current.value;var i=Math.min(Math.abs(this.source.maxLevel),Math.abs(Math.floor(Math.log(t/this.minPixelRatio)/Math.log(2))));i=Math.max(i,this.source.minLevel||0);return{lowestLevel:e=Math.min(e,i),highestLevel:i}},_updateViewport:function(){this._needsDraw=!1;this._tilesLoading=0;this.loadingCoverage={};for(;0=this.minPixelRatio)a=c=!0;else if(!a)continue;var d=e.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(h),!1).x*this._scaleSpring.current.value;var p=e.deltaPixelsFromPointsNoRotate(this.source.getPixelRatio(Math.max(this.source.getClosestLevel(),0)),!1).x*this._scaleSpring.current.value;var g=this.immediateRender?1:p;s=m(this,a,c,h,Math.min(1,(u-.5)/.5),g/Math.abs(g-d),t,l,s);if(f(this.coverage,h))break}!function(n,e){if(0===n.opacity||0===e.length&&!n.placeholderFillStyle)return;var t=e[0];var i;t&&(i=n.opacity<1||n.compositeOperation&&"source-over"!==n.compositeOperation||!n._isBottomItem()&&t._hasTransparencyChannel());var o;var r;var s=n.viewport.getZoom(!0);var a=n.viewportToImageZoom(s);if(1n.smoothTileEdgesMinZoom&&!n.iOSDevice&&n.getRotation(!0)%360==0&&y.supportsCanvas){i=!0;o=t.getScaleForEdgeSmoothing();r=t.getTranslationForEdgeSmoothing(o,n._drawer.getCanvasSize(!1),n._drawer.getCanvasSize(!0))}var l;if(i){if(!o){l=n.viewport.viewportToViewerElementRectangle(n.getClippedBounds(!0)).getIntegerBoundingBox();n._drawer.viewer.viewport.getFlip()&&(0===n.viewport.degrees&&n.getRotation(!0)%360==0||(l.x=n._drawer.viewer.container.clientWidth-(l.x+l.width)));l=l.times(y.pixelDensityRatio)}n._drawer._clear(!0,l)}if(!o){0!==n.viewport.degrees&&n._drawer._offsetForRotation({degrees:n.viewport.degrees,useSketch:i});n.getRotation(!0)%360!=0&&n._drawer._offsetForRotation({degrees:n.getRotation(!0),point:n.viewport.pixelFromPointNoRotate(n._getRotationPoint(!0),!0),useSketch:i});0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip()}var h=!1;if(n._clip){n._drawer.saveContext(i);var c=n.imageToViewportRectangle(n._clip,!0);c=c.rotate(-n.getRotation(!0),n._getRotationPoint(!0));var u=n._drawer.viewportToDrawerRectangle(c);o&&(u=u.times(o));r&&(u=u.translate(r));n._drawer.setClip(u,i);h=!0}if(n._croppingPolygons){n._drawer.saveContext(i);try{var d=n._croppingPolygons.map(function(e){return e.map(function(e){var t=n.imageToViewportCoordinates(e.x,e.y,!0).rotate(-n.getRotation(!0),n._getRotationPoint(!0));var i=n._drawer.viewportCoordToDrawerCoord(t);o&&(i=i.times(o));return i})});n._drawer.clipWithPolygons(d,i)}catch(e){y.console.error(e)}h=!0}if(n.placeholderFillStyle&&!1===n._hasOpaqueTile){var p=n._drawer.viewportToDrawerRectangle(n.getBounds(!0));o&&(p=p.times(o));r&&(p=p.translate(r));var g=null;g="function"==typeof n.placeholderFillStyle?n.placeholderFillStyle(n,n._drawer.context):n.placeholderFillStyle;n._drawer.drawRectangle(p,g,i)}for(var m=e.length-1;0<=m;m--){t=e[m];n._drawer.drawTile(t,n._drawingHandler,i,o,r);t.beingDrawn=!0;n.viewer&&n.viewer.raiseEvent("tile-drawn",{tiledImage:n,tile:t})}h&&n._drawer.restoreContext(i);if(!o){n.getRotation(!0)%360!=0&&n._drawer._restoreRotationChanges(i);0!==n.viewport.degrees&&n._drawer._restoreRotationChanges(i)}if(i){if(o){0!==n.viewport.degrees&&n._drawer._offsetForRotation({degrees:n.viewport.degrees,useSketch:!1});n.getRotation(!0)%360!=0&&n._drawer._offsetForRotation({degrees:n.getRotation(!0),point:n.viewport.pixelFromPointNoRotate(n._getRotationPoint(!0),!0),useSketch:!1})}n._drawer.blendSketch({opacity:n.opacity,scale:o,translate:r,compositeOperation:n.compositeOperation,bounds:l});if(o){n.getRotation(!0)%360!=0&&n._drawer._restoreRotationChanges(!1);0!==n.viewport.degrees&&n._drawer._restoreRotationChanges(!1)}}o||0===n.viewport.degrees&&n.getRotation(!0)%360==0&&n._drawer.viewer.viewport.getFlip()&&n._drawer._flip();!function(e,t){if(e.debugMode)for(var i=t.length-1;0<=i;i--){var n=t[i];try{e._drawer.drawDebugInfo(n,t.length,i,e)}catch(e){y.console.error(e)}}}(n,e)}(this,this.lastDrawn);if(s&&!s.context2D){!function(n,o,r){o.loading=!0;n._imageLoader.addJob({src:o.url,loadWithAjax:o.loadWithAjax,ajaxHeaders:o.ajaxHeaders,crossOriginPolicy:n.crossOriginPolicy,ajaxWithCredentials:n.ajaxWithCredentials,callback:function(e,t,i){!function(t,i,e,n,o,r){if(!n){y.console.log("Tile %s failed to load: %s - error: %s",i,i.url,o);t.viewer.raiseEvent("tile-load-failed",{tile:i,tiledImage:t,time:e,message:o,tileRequest:r});i.loading=!1;i.exists=!1;return}if(ee.visibility)return t;if(t.visibility==e.visibility&&t.squaredDistancethis._maxImageCacheCount){var o=null;var r=-1;var s=null;var a,l,h,c,u,d;for(var p=this._tilesLoaded.length-1;0<=p;p--)if(!((a=(d=this._tilesLoaded[p]).tile).level<=t||a.beingDrawn))if(o){c=a.lastTouchTime;l=o.lastTouchTime;u=a.level;h=o.level;if(c=this._items.length)throw new Error("Index bigger than number of layers.");if(t!==i&&-1!==i){this._items.splice(i,1);this._items.splice(t,0,e);this._needsDraw=!0;this.raiseEvent("item-index-change",{item:e,previousIndex:i,newIndex:t})}},removeItem:function(e){v.console.assert(e,"[World.removeItem] item is required");var t=v.indexOf(this._items,e);if(-1!==t){e.removeHandler("bounds-change",this._delegatedFigureSizes);e.removeHandler("clip-change",this._delegatedFigureSizes);e.destroy();this._items.splice(t,1);this._figureSizes();this._needsDraw=!0;this._raiseRemoveItem(e)}},removeAll:function(){this.viewer._cancelPendingImages();var e;var t;for(t=0;tu.height?r:r*(u.width/u.height))*(u.height/u.width);g=new v.Point(l+(r-d)/2,h+(r-p)/2);c.setPosition(g,t);c.setWidth(d,t);"horizontal"===i?l+=s:h+=s}this.setAutoRefigureSizes(!0)},_figureSizes:function(){var e=this._homeBounds?this._homeBounds.clone():null;var t=this._contentSize?this._contentSize.clone():null;var i=this._contentFactor||0;if(this._items.length){var n=this._items[0];var o=n.getBounds();this._contentFactor=n.getContentSize().x/o.width;var r=n.getClippedBounds().getBoundingBox();var s=r.x;var a=r.y;var l=r.x+r.width;var h=r.y+r.height;for(var c=1;c + * window.segmentationUI – palette controller + */ +(function () { + 'use strict'; + + const TILE_SIZE = 256; + + function hexToRgb(hex) { + const v = parseInt(hex.replace('#', ''), 16); + return { r: (v >> 16) & 0xff, g: (v >> 8) & 0xff, b: v & 0xff }; + } + + function getCsrf() { + const m = document.cookie.match(/csrftoken=([^;]+)/); + return m ? m[1] : ''; + } + + // ── SegmentationLayer ──────────────────────────────────────────────── + + class SegmentationLayer { + constructor(viewer, annotationId, imageWidth, imageHeight, color, frame) { + this.viewer = viewer; + this.annotationId = annotationId; + this.imageWidth = imageWidth; + this.imageHeight = imageHeight; + this.color = hexToRgb(color); + this.frame = frame || 0; + this.opacity = 0.55; + this.editable = false; + + // Tool state + this.activeTool = null; + this.brushSize = 20; + this.brushShape = 'circle'; + this.tolerance = 20; + this.activeClass = 1; + + // Drawing state + this.isDrawing = false; + this.lastImgX = null; + this.lastImgY = null; + + // Undo state + this._undoStack = []; + this._preStrokeSnap = null; + + // Tile state + this.tileCache = new Map(); // `${tx}_${ty}` → Uint8Array | null(loading) + this.renderCache = new Map(); // `${tx}_${ty}` → ImageBitmap (colored, ready to blit) + this.dirtyTiles = new Set(); + this.imgTileCache = new Map(); // `${tx}_${ty}` → ImageBitmap (raw image pixels) + this._dziMaxLevel = null; + this._dziTileSize = 254; + + this._setupCanvases(); + this._bindViewerEvents(); + this._bindDrawEvents(); + // Trigger initial draw after OSD has had a chance to open its tile source + this.viewer.addOnceHandler('open', () => this._redraw()); + if (this.viewer.world.getItemAt(0)) this._redraw(); + } + + // ── Canvas setup ───────────────────────────────────────────────── + + _setupCanvases() { + const container = this.viewer.canvas.parentElement; + + // Display canvas lives in image-coordinate space (capped to avoid + // excessive memory on gigapixel slides). Its CSS transform is updated + // every frame to track the viewport — zero canvas-redraw cost during pan/zoom. + const MAX_DIM = 4096; + this._dispScale = Math.min(1, + MAX_DIM / Math.max(this.imageWidth, this.imageHeight)); + this.displayCanvas = document.createElement('canvas'); + this.displayCanvas.width = Math.ceil(this.imageWidth * this._dispScale); + this.displayCanvas.height = Math.ceil(this.imageHeight * this._dispScale); + Object.assign(this.displayCanvas.style, { + position: 'absolute', top: '0', left: '0', + transformOrigin: '0 0', + pointerEvents: 'none', + opacity: this.opacity, + zIndex: '10', + }); + container.appendChild(this.displayCanvas); + + // Draw canvas stays at screen resolution — it captures mouse events + // and renders the brush cursor. + this.drawCanvas = document.createElement('canvas'); + Object.assign(this.drawCanvas.style, { + position: 'absolute', top: '0', left: '0', + zIndex: '11', + display: 'none', + cursor: 'crosshair', + }); + container.appendChild(this.drawCanvas); + + this._resizeCanvases(); + } + + _resizeCanvases() { + // Only the draw canvas needs resizing; displayCanvas is in image space. + const w = this.viewer.canvas.clientWidth || this.viewer.canvas.offsetWidth; + const h = this.viewer.canvas.clientHeight || this.viewer.canvas.offsetHeight; + this.drawCanvas.width = w; this.drawCanvas.height = h; + this.drawCanvas.style.width = w + 'px'; this.drawCanvas.style.height = h + 'px'; + } + + // ── OSD event bindings ─────────────────────────────────────────── + + _bindViewerEvents() { + this._onUpdate = () => this._redraw(); + this._onResize = () => { this._resizeCanvases(); this._redraw(); }; + this._onPage = (e) => { this.setFrame(e.page); }; + // 'animation' fires after OSD completes its full draw pass each frame, + // in sync with its rAF loop — the same pattern used by the scalebar, + // quad-tree, and guides plugins. 'animation-finish' catches the last frame. + this.viewer.addHandler('animation', this._onUpdate); + this.viewer.addHandler('animation-finish', this._onUpdate); + this.viewer.addHandler('resize', this._onResize); + this.viewer.addHandler('page', this._onPage); + } + + // ── Draw canvas events ─────────────────────────────────────────── + + _bindDrawEvents() { + const c = this.drawCanvas; + + c.addEventListener('mousedown', (e) => { + if (e.button !== 0) return; + e.preventDefault(); e.stopPropagation(); + this.isDrawing = true; + const [ix, iy] = this._vpToImg(e.offsetX, e.offsetY); + this.lastImgX = ix; this.lastImgY = iy; + + if (this.activeTool === 'wand') { + this._preStrokeSnap = this._snapshotTileCache(); + this._magicWand(ix, iy); + this._commitUndo(); + this._uploadDirtyTiles(); + } else if (this.activeTool === 'fill') { + this._preStrokeSnap = this._snapshotTileCache(); + this._fillTool(ix, iy); + this._commitUndo(); + this._uploadDirtyTiles(); + } else { + this._preStrokeSnap = this._snapshotTileCache(); + this._paintBrush(ix, iy, ix, iy); + } + }); + + c.addEventListener('mousemove', (e) => { + this._updateCursor(e.offsetX, e.offsetY); + if (!this.isDrawing) return; + if (this.activeTool === 'wand' || this.activeTool === 'fill') return; + const [ix, iy] = this._vpToImg(e.offsetX, e.offsetY); + this._paintBrush(this.lastImgX, this.lastImgY, ix, iy); + this.lastImgX = ix; this.lastImgY = iy; + }); + + c.addEventListener('mouseleave', () => { + this._cursorVpX = null; + this._redraw(); + if (!this.isDrawing) return; + this.isDrawing = false; + this._commitUndo(); + this._uploadDirtyTiles(); + }); + + c.addEventListener('mouseup', () => { + if (!this.isDrawing) return; + this.isDrawing = false; + this._commitUndo(); + this._uploadDirtyTiles(); + }); + + this._onKeyDown = (e) => { + if (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') return; + if ((e.ctrlKey || e.metaKey) && e.key === 'z') { + e.preventDefault(); + this.undo(); + } + }; + document.addEventListener('keydown', this._onKeyDown); + } + + // ── Coordinates ────────────────────────────────────────────────── + + _vpToImg(vpX, vpY) { + const vp = this.viewer.viewport; + const pt = vp.pointFromPixel(new OpenSeadragon.Point(vpX, vpY)); + const ip = this.viewer.world.getItemAt(0).viewportToImageCoordinates(pt); + return [Math.round(ip.x), Math.round(ip.y)]; + } + + _imgToScreen(ix, iy) { + const vp = this.viewer.viewport; + const vpt = this.viewer.world.getItemAt(0) + .imageToViewportCoordinates(new OpenSeadragon.Point(ix, iy)); + return vp.pixelFromPoint(vpt); + } + + // ── Tile cache ─────────────────────────────────────────────────── + + _tileKey(tx, ty) { return `${tx}_${ty}`; } + + _getTileData(tx, ty) { + const k = this._tileKey(tx, ty); + if (!this.tileCache.has(k)) this.tileCache.set(k, new Uint8Array(TILE_SIZE * TILE_SIZE)); + return this.tileCache.get(k); + } + + async _ensureTile(tx, ty) { + const k = this._tileKey(tx, ty); + if (this.tileCache.has(k)) return; + this.tileCache.set(k, null); + try { + const r = await fetch( + `/annotations/api/segmentation/${this.annotationId}/tiles/0/${tx}/${ty}/?frame=${this.frame}`, + { credentials: 'same-origin' }); + if (r.status === 204) { + this.tileCache.set(k, new Uint8Array(TILE_SIZE * TILE_SIZE)); + } else { + const bm = await createImageBitmap(await r.blob()); + const oc = new OffscreenCanvas(TILE_SIZE, TILE_SIZE); + const ctx = oc.getContext('2d'); + ctx.drawImage(bm, 0, 0); + const raw = ctx.getImageData(0, 0, TILE_SIZE, TILE_SIZE).data; + const labels = new Uint8Array(TILE_SIZE * TILE_SIZE); + // Threshold at 127: robust against gamma correction and any + // color-space transform the browser applies during PNG decode. + for (let i = 0; i < labels.length; i++) labels[i] = raw[i * 4] > 127 ? 1 : 0; + this.tileCache.set(k, labels); + this._prerenderTile(tx, ty); // draw into displayCanvas immediately + } + } catch (_) { this.tileCache.delete(k); } + // No full _redraw() needed — tile was painted directly into displayCanvas. + } + + // ── Display ────────────────────────────────────────────────────── + + // Render one tile into the display canvas (image-coordinate space). + // Called once when tile data arrives or changes — NOT every frame. + _prerenderTile(tx, ty) { + const k = this._tileKey(tx, ty); + const labels = this.tileCache.get(k); + if (!labels) return; + const { r, g, b } = this.color; + const oc = new OffscreenCanvas(TILE_SIZE, TILE_SIZE); + const ctx = oc.getContext('2d'); + const id = ctx.createImageData(TILE_SIZE, TILE_SIZE); + const d = id.data; + let hasContent = false; + for (let i = 0; i < labels.length; i++) { + if (labels[i] > 0) { + d[i*4]=r; d[i*4+1]=g; d[i*4+2]=b; d[i*4+3]=200; + hasContent = true; + } + } + ctx.putImageData(id, 0, 0); + const s = this._dispScale; + const dCtx = this.displayCanvas.getContext('2d'); + // Erase the old content for this tile slot first + dCtx.clearRect(tx * TILE_SIZE * s, ty * TILE_SIZE * s, TILE_SIZE * s, TILE_SIZE * s); + if (hasContent) { + createImageBitmap(oc).then(bm => { + this.renderCache.set(k, bm); + dCtx.drawImage(bm, tx * TILE_SIZE * s, ty * TILE_SIZE * s, + TILE_SIZE * s, TILE_SIZE * s); + }); + } else { + this.renderCache.delete(k); + } + } + + // Redraw entire display canvas from renderCache (used on color/frame change). + _rerenderDisplay() { + const dCtx = this.displayCanvas.getContext('2d'); + dCtx.clearRect(0, 0, this.displayCanvas.width, this.displayCanvas.height); + const s = this._dispScale; + this.renderCache.forEach((bm, k) => { + const [tx, ty] = k.split('_').map(Number); + dCtx.drawImage(bm, tx * TILE_SIZE * s, ty * TILE_SIZE * s, + TILE_SIZE * s, TILE_SIZE * s); + }); + } + + // Called every animation frame — updates the CSS transform only. + // No canvas pixel work; the GPU compositor moves the overlay for free. + _redraw() { + const item = this.viewer.world.getItemAt(0); + if (!item) return; + + // Two image-corner → screen mappings give us translate + scale. + const p00 = this._imgToScreen(0, 0); + const p11 = this._imgToScreen(this.imageWidth, this.imageHeight); + const sx = (p11.x - p00.x) / this.displayCanvas.width; + const sy = (p11.y - p00.y) / this.displayCanvas.height; + this.displayCanvas.style.transform = + `translate(${p00.x}px,${p00.y}px) scale(${sx},${sy})`; + + // Cursor lives on the draw canvas (screen space) — cheap clear+draw. + const dCtx = this.drawCanvas.getContext('2d'); + dCtx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); + if (this._cursorVpX != null && this.activeTool && + this.activeTool !== 'wand' && this.activeTool !== 'fill' && + this.activeTool !== 'pan') { + this._drawCursorShape(dCtx, this._cursorVpX, this._cursorVpY); + } + + // Trigger tile fetches for newly visible tiles (display canvas will + // update incrementally as each tile arrives). + const vp = this.viewer.viewport; + const bounds = vp.getBounds(true); + const tlImg = item.viewportToImageCoordinates( + new OpenSeadragon.Point(bounds.x, bounds.y)); + const brImg = item.viewportToImageCoordinates( + new OpenSeadragon.Point(bounds.x + bounds.width, bounds.y + bounds.height)); + const tx0 = Math.max(0, Math.floor(tlImg.x / TILE_SIZE)); + const ty0 = Math.max(0, Math.floor(tlImg.y / TILE_SIZE)); + const tx1 = Math.min(Math.ceil(this.imageWidth / TILE_SIZE) - 1, + Math.ceil(brImg.x / TILE_SIZE)); + const ty1 = Math.min(Math.ceil(this.imageHeight / TILE_SIZE) - 1, + Math.ceil(brImg.y / TILE_SIZE)); + for (let ty = ty0; ty <= ty1; ty++) + for (let tx = tx0; tx <= tx1; tx++) + if (!this.tileCache.has(this._tileKey(tx, ty))) + this._ensureTile(tx, ty); + } + + _updateCursor(vpX, vpY) { + this._cursorVpX = vpX; + this._cursorVpY = vpY; + // Cursor lives on the draw canvas — update it directly without touching displayCanvas. + const dCtx = this.drawCanvas.getContext('2d'); + dCtx.clearRect(0, 0, this.drawCanvas.width, this.drawCanvas.height); + if (this.activeTool && this.activeTool !== 'wand' && + this.activeTool !== 'fill' && this.activeTool !== 'pan') { + this._drawCursorShape(dCtx, vpX, vpY); + } + } + + _drawCursorShape(ctx, vpX, vpY) { + const item = this.viewer.world.getItemAt(0); + if (!item) return; + const vp = this.viewer.viewport; + const pt0 = vp.pixelFromPoint(item.imageToViewportCoordinates( + new OpenSeadragon.Point(0, 0))); + const pt1 = vp.pixelFromPoint(item.imageToViewportCoordinates( + new OpenSeadragon.Point(this.brushSize, this.brushSize))); + const rX = Math.abs(pt1.x - pt0.x) / 2; + const rY = Math.abs(pt1.y - pt0.y) / 2; + + ctx.save(); + ctx.strokeStyle = 'rgba(255,255,255,0.85)'; + ctx.lineWidth = 1.5; + ctx.setLineDash([4, 3]); + ctx.beginPath(); + if (this.brushShape === 'circle') { + ctx.ellipse(vpX, vpY, Math.max(1,rX), Math.max(1,rY), 0, 0, Math.PI*2); + } else { + ctx.rect(vpX - rX, vpY - rY, rX*2, rY*2); + } + ctx.stroke(); + ctx.restore(); + } + + // ── Paint tools ────────────────────────────────────────────────── + + _paintBrush(x0, y0, x1, y1) { + let dx = Math.abs(x1-x0), dy = Math.abs(y1-y0); + const sx = x0-dy){err-=dy; x+=sx;} + if (e2< dx){err+=dx; y+=sy;} + } + this._rerenderDirty(); + } + + _stamp(cx, cy) { + const r = Math.max(1, Math.floor(this.brushSize/2)); + const label = this.activeTool === 'eraser' ? 0 : this.activeClass; + const r2 = r*r; + for (let dy=-r; dy<=r; dy++) { + for (let dx=-r; dx<=r; dx++) { + if (this.brushShape==='circle' && dx*dx+dy*dy>r2) continue; + this._setPixel(cx+dx, cy+dy, label); + } + } + } + + _setPixel(ix, iy, label) { + if (ix<0||iy<0||ix>=this.imageWidth||iy>=this.imageHeight) return; + const tx = Math.floor(ix/TILE_SIZE), ty = Math.floor(iy/TILE_SIZE); + const k = this._tileKey(tx, ty); + if (!this.tileCache.has(k)) this.tileCache.set(k, new Uint8Array(TILE_SIZE*TILE_SIZE)); + const labels = this.tileCache.get(k); + if (!labels) return; + labels[(iy%TILE_SIZE)*TILE_SIZE + (ix%TILE_SIZE)] = label; + this.dirtyTiles.add(k); + this.renderCache.delete(k); // mark render stale; rebuilt in _rerenderDirty + } + + _rerenderDirty() { + // Re-render all tiles modified since last call, then redraw. + // Called once per brush stroke / fill, not per pixel. + const toRender = new Set([...this.dirtyTiles]); + let pending = toRender.size; + if (pending === 0) { this._redraw(); return; } + toRender.forEach(k => { + const [tx, ty] = k.split('_').map(Number); + this._prerenderTile(tx, ty); + }); + } + + _getPixel(ix, iy) { + if (ix<0||iy<0||ix>=this.imageWidth||iy>=this.imageHeight) return -1; + const labels = this._getTileData(Math.floor(ix/TILE_SIZE), Math.floor(iy/TILE_SIZE)); + return labels[(iy%TILE_SIZE)*TILE_SIZE + (ix%TILE_SIZE)]; + } + + // ── Undo ────────────────────────────────────────────────────────── + + _snapshotTileCache() { + const snap = new Map(); + this.tileCache.forEach((data, k) => { + snap.set(k, data ? data.slice() : null); + }); + return snap; + } + + _commitUndo() { + if (!this._preStrokeSnap) return; + this._undoStack.push(this._preStrokeSnap); + if (this._undoStack.length > 20) this._undoStack.shift(); + this._preStrokeSnap = null; + } + + undo() { + if (!this._undoStack.length) return; + const snap = this._undoStack.pop(); + // Clear display canvas first so stroke-created tiles are visually removed. + const dCtx = this.displayCanvas.getContext('2d'); + dCtx.clearRect(0, 0, this.displayCanvas.width, this.displayCanvas.height); + // Restore tile data from snapshot (overwrites current state). + this.tileCache = snap; + this.renderCache.clear(); + this.dirtyTiles.clear(); + // Re-render every tile in the restored snapshot. + snap.forEach((data, k) => { + if (!data) return; + const [tx, ty] = k.split('_').map(Number); + this._prerenderTile(tx, ty); + }); + } + + // ── Fill tool (mask flood fill) ─────────────────────────────────── + + _fillTool(startX, startY) { + this._imageColorBFS(startX, startY, /* skipLabeled */ false); + this._rerenderDirty(); + } + + _magicWand(startX, startY) { + this._imageColorBFS(startX, startY, /* skipLabeled */ false); + this._rerenderDirty(); + } + + // ── Image-color BFS (shared by magic wand and fill) ─────────────── + // Samples OSD's rendered canvas once, then floods connected pixels whose + // color distance from the seed is within this.tolerance. + // skipLabeled=true → skip pixels already painted with any label. + + _imageColorBFS(startX, startY, skipLabeled) { + const dc = this.viewer.drawer.canvas; + if (!dc) { console.warn('Seg: OSD canvas not available'); return; } + + const CW = dc.width, CH = dc.height; + let pixels; + try { + pixels = dc.getContext('2d').getImageData(0, 0, CW, CH).data; + } catch (e) { + console.warn('Seg: cannot read OSD canvas (cross-origin?)', e); + return; + } + + // Affine image-pixel → physical-canvas-pixel transform. + // _imgToScreen returns CSS pixels; multiply by devicePixelRatio to get + // physical canvas pixels (needed for correct indexing on HiDPI displays). + const dpr = window.devicePixelRatio || 1; + const p00 = this._imgToScreen(0, 0); + const p10 = this._imgToScreen(1, 0); + const p01 = this._imgToScreen(0, 1); + const dxX = (p10.x - p00.x) * dpr; + const dyY = (p01.y - p00.y) * dpr; + const ox = p00.x * dpr; + const oy = p00.y * dpr; + + const sample = (ix, iy) => { + const sx = Math.round(ox + ix * dxX); + const sy = Math.round(oy + iy * dyY); + if (sx < 0 || sx >= CW || sy < 0 || sy >= CH) return null; + const idx = (sy * CW + sx) * 4; + return [pixels[idx], pixels[idx+1], pixels[idx+2]]; + }; + + const ref = sample(startX, startY); + if (!ref) { + console.warn('Seg: seed pixel outside rendered canvas — zoom in first'); + return; + } + const [refR, refG, refB] = ref; + const tol = this.tolerance; + + const W = this.imageWidth, H = this.imageHeight; + const nPx = W * H; + const visited = nPx <= 4 * 1024 * 1024 + ? new Uint8Array(nPx) + : new Set(); + const see = nPx <= 4 * 1024 * 1024 + ? (x, y) => visited[y * W + x] + : (x, y) => visited.has(y * W + x); + const mark = nPx <= 4 * 1024 * 1024 + ? (x, y) => { visited[y * W + x] = 1; } + : (x, y) => visited.add(y * W + x); + + const queue = [startX, startY]; + let head = 0; + mark(startX, startY); + const MAX_FILL = 2_000_000; + let filled = 0; + + while (head < queue.length && filled < MAX_FILL) { + const cx = queue[head++], cy = queue[head++]; + + if (skipLabeled && this._getPixel(cx, cy) !== 0) continue; + + const col = sample(cx, cy); + if (!col) continue; + // Per-channel max-distance tolerance (matches classic magic-wand feel) + if (Math.abs(col[0] - refR) > tol || + Math.abs(col[1] - refG) > tol || + Math.abs(col[2] - refB) > tol) continue; + + this._setPixel(cx, cy, this.activeClass); + filled++; + + if (cx > 0 && !see(cx-1, cy)) { mark(cx-1, cy); queue.push(cx-1, cy); } + if (cx < W-1 && !see(cx+1, cy)) { mark(cx+1, cy); queue.push(cx+1, cy); } + if (cy > 0 && !see(cx, cy-1)) { mark(cx, cy-1); queue.push(cx, cy-1); } + if (cy < H-1 && !see(cx, cy+1)) { mark(cx, cy+1); queue.push(cx, cy+1); } + } + } + + // ── Upload ──────────────────────────────────────────────────────── + + async _uploadDirtyTiles() { + const keys = Array.from(this.dirtyTiles); + this.dirtyTiles.clear(); + const csrf = getCsrf(); + await Promise.all(keys.map(async k => { + const [tx, ty] = k.split('_').map(Number); + const labels = this.tileCache.get(k); + if (!labels) return; + const blob = await this._encodePNG(labels); + const url = `/annotations/api/segmentation/${this.annotationId}/tiles/0/${tx}/${ty}/?frame=${this.frame}`; + await fetch(url, { + method: 'PUT', credentials: 'same-origin', + headers: {'Content-Type':'image/png','X-CSRFToken':csrf}, + body: blob, + }).catch(() => this.dirtyTiles.add(k)); + })); + } + + async _encodePNG(labels) { + const oc = new OffscreenCanvas(TILE_SIZE, TILE_SIZE); + const ctx = oc.getContext('2d'); + const id = ctx.createImageData(TILE_SIZE, TILE_SIZE); + for (let i = 0; i < labels.length; i++) { + // Use 0/255 extremes (not 0/1) — immune to gamma correction and + // PIL's RGBA→L alpha-composite-on-white behaviour. + // Always fully opaque so alpha compositing cannot corrupt values. + const pv = labels[i] > 0 ? 255 : 0; + id.data[i*4] = pv; + id.data[i*4+1] = pv; + id.data[i*4+2] = pv; + id.data[i*4+3] = 255; + } + ctx.putImageData(id, 0, 0); + return oc.convertToBlob({type:'image/png'}); + } + + // ── Public interface ────────────────────────────────────────────── + + setTool(tool) { + this.activeTool = tool; + this.drawCanvas.style.display = (tool && this.editable) ? 'block' : 'none'; + this.drawCanvas.style.cursor = tool==='eraser' ? 'cell' : 'crosshair'; + } + + setEditable(editable) { + this.editable = editable; + if (!editable) { + this.drawCanvas.style.display = 'none'; + this.isDrawing = false; + } else if (this.activeTool) { + this.drawCanvas.style.display = 'block'; + } + } + + setBrushSize(px) { this.brushSize = px; } + setBrushShape(sh) { this.brushShape = sh; } + setTolerance(t) { this.tolerance = t; } + setActiveClass(c) { this.activeClass = c; } + setOpacity(o) { this.opacity = o; this.displayCanvas.style.opacity = o; } + setColor(hex) { + this.color = hexToRgb(hex); + this.renderCache.clear(); + // Re-render all tiles with new color, then repaint display canvas. + const rerender = () => { + this.tileCache.forEach((labels, k) => { + if (!labels) return; + const [tx, ty] = k.split('_').map(Number); + this._prerenderTile(tx, ty); + }); + }; + rerender(); + } + setVisible(v) { this.displayCanvas.style.display = v ? 'block' : 'none'; } + + setFrame(frame) { + this.frame = frame; + this.tileCache.clear(); this.renderCache.clear(); + this.dirtyTiles.clear(); this.imgTileCache.clear(); + this._dziMaxLevel = null; + // Clear display canvas; tiles will refill as they load via _ensureTile. + const dCtx = this.displayCanvas.getContext('2d'); + dCtx.clearRect(0, 0, this.displayCanvas.width, this.displayCanvas.height); + this._redraw(); // update CSS transform for new viewport position + } + + destroy() { + this.viewer.removeHandler('animation', this._onUpdate); + this.viewer.removeHandler('animation-finish', this._onUpdate); + this.viewer.removeHandler('resize', this._onResize); + this.viewer.removeHandler('page', this._onPage); + if (this._onKeyDown) document.removeEventListener('keydown', this._onKeyDown); + this.displayCanvas.remove(); + this.drawCanvas.remove(); + } + } + + // ── SegmentationUI ──────────────────────────────────────────────────── + + const segmentationUI = { + _activeTypeId: null, + + init() { + const panel = document.getElementById('seg-palette'); + if (!panel) return; + + // Tool buttons + document.querySelectorAll('.seg-tool-btn').forEach(btn => { + btn.addEventListener('click', () => { + const tool = btn.dataset.tool; + // 'pan' = deactivate draw canvas, let OSD handle events + if (tool === 'pan') { + this._setActiveToolBtn(btn); + const layer = window.segmentationLayers.get(this._activeTypeId); + if (layer) layer.setTool(null); + } else { + this._setActiveToolBtn(btn); + const layer = window.segmentationLayers.get(this._activeTypeId); + if (layer) layer.setTool(tool); + } + // Show/hide tolerance row + const tolRow = document.getElementById('seg-tolerance-row'); + if (tolRow) tolRow.style.display = + (tool==='wand'||tool==='fill') ? 'block' : 'none'; + }); + }); + + // Brush size + const bs = document.getElementById('seg-brush-size'); + if (bs) bs.addEventListener('input', () => { + document.getElementById('seg-brush-size-val').textContent = bs.value; + window.segmentationLayers.forEach(l => l.setBrushSize(parseInt(bs.value))); + }); + + // Brush shape + document.querySelectorAll('.seg-shape-btn').forEach(btn => { + btn.addEventListener('click', () => { + document.querySelectorAll('.seg-shape-btn').forEach(b => { + b.classList.remove('btn-light'); b.classList.add('btn-outline-light'); + }); + btn.classList.remove('btn-outline-light'); btn.classList.add('btn-light'); + window.segmentationLayers.forEach(l => l.setBrushShape(btn.dataset.shape)); + }); + }); + + // Tolerance + const tol = document.getElementById('seg-tolerance'); + if (tol) tol.addEventListener('input', () => { + document.getElementById('seg-tolerance-val').textContent = tol.value; + window.segmentationLayers.forEach(l => l.setTolerance(parseInt(tol.value))); + }); + + // Opacity + const op = document.getElementById('seg-opacity'); + if (op) op.addEventListener('input', () => { + document.getElementById('seg-opacity-val').textContent = op.value + '%'; + window.segmentationLayers.forEach(l => l.setOpacity(parseFloat(op.value)/100)); + }); + }, + + _setActiveToolBtn(activeBtn) { + document.querySelectorAll('.seg-tool-btn').forEach(b => { + b.classList.remove('btn-light'); b.classList.add('btn-outline-light'); + }); + activeBtn.classList.remove('btn-outline-light'); + activeBtn.classList.add('btn-light'); + }, + + activateType(typeId) { + this._activeTypeId = typeId; + // Make all layers view-only, then make the selected one editable + window.segmentationLayers.forEach((layer, tid) => { + layer.setEditable(tid === typeId); + }); + // Default to pan so navigation doesn't cause accidental strokes. + // Only switch to pan if no drawing tool is currently active. + const activeBtn = document.querySelector('.seg-tool-btn.btn-light'); + if (!activeBtn || activeBtn.dataset.tool === 'pencil') { + const panBtn = document.querySelector('.seg-tool-btn[data-tool="pan"]'); + if (panBtn) panBtn.dispatchEvent(new MouseEvent('click', {bubbles:true})); + } + }, + + deactivateAll() { + this._activeTypeId = null; + window.segmentationLayers.forEach(l => l.setEditable(false)); + }, + }; + + // ── Global management ───────────────────────────────────────────────── + + window.segmentationLayers = new Map(); + window.segmentationUI = segmentationUI; + let _activeViewer = null; // tracks which OSD viewer the layers belong to + + // When a new image is loaded a new OSD viewer is created and exactViewerReady fires. + // Destroy all layers from the previous image so stale canvases don't accumulate. + window.addEventListener('exactViewerReady', () => { + if (_activeViewer && _activeViewer !== window.exactOSDViewer) { + window.destroyAllSegmentationLayers(); + } + _activeViewer = window.exactOSDViewer; + }); + + window.activateSegmentationLayer = async function ( + annotationTypeId, color, imageId, imageWidth, imageHeight, frame + ) { + // Show palette + const panel = document.getElementById('seg-palette'); + if (panel) panel.style.display = 'flex'; + + // Get OSD viewer (set on window by EXACTViewer constructor) + const viewer = window.exactOSDViewer; + if (!viewer) { console.error('OSD viewer not available'); return; } + + // Guard: if the viewer changed since layers were created, start fresh. + if (_activeViewer && _activeViewer !== viewer) { + window.destroyAllSegmentationLayers(); + } + _activeViewer = viewer; + + let layer = window.segmentationLayers.get(annotationTypeId); + + if (!layer) { + // Find or create annotation on server + let annotationId = null; + try { + const r = await fetch( + `/api/v1/annotations/annotations/?image=${imageId}&annotation_type=${annotationTypeId}&deleted=False&format=json`, + { credentials: 'same-origin' }); + const data = await r.json(); + if (data.results?.length > 0) { + annotationId = data.results[0].id; + } else { + const cr = await fetch('/api/v1/annotations/annotations/', { + method: 'POST', credentials: 'same-origin', + headers: {'Content-Type':'application/json','X-CSRFToken':getCsrf()}, + body: JSON.stringify({ + image: imageId, annotation_type: annotationTypeId, + vector: {tile_size: TILE_SIZE, width: imageWidth, height: imageHeight}, + }), + }); + annotationId = (await cr.json()).id; + } + } catch (e) { console.error('Could not get/create annotation', e); return; } + + layer = new SegmentationLayer( + viewer, annotationId, imageWidth, imageHeight, color, frame); + window.segmentationLayers.set(annotationTypeId, layer); + } else { + if (layer.frame !== frame) layer.setFrame(frame); + } + + segmentationUI.activateType(annotationTypeId); + }; + + window.deactivateSegmentationLayer = function () { + segmentationUI.deactivateAll(); + const panel = document.getElementById('seg-palette'); + if (panel) panel.style.display = 'none'; + }; + + window.destroyAllSegmentationLayers = function () { + window.segmentationLayers.forEach(l => l.destroy()); + window.segmentationLayers.clear(); + segmentationUI.deactivateAll(); + }; + + document.addEventListener('DOMContentLoaded', () => segmentationUI.init()); + +})(); diff --git a/exact/exact/annotations/urls.py b/exact/exact/annotations/urls.py index 1d316e6c..b9c84b68 100644 --- a/exact/exact/annotations/urls.py +++ b/exact/exact/annotations/urls.py @@ -34,4 +34,7 @@ re_path(r'^api/annotation/mediafile/upload/(\d+)/(\d+)/$', views.api_create_annotation_mediafile, name='api_create_annotation_mediafile'), re_path(r'^api/annotation/mediafile/delete/$', views.api_delete_annotation_mediafile, name='api_delete_annotation_mediafile'), re_path(r'^api/annotation/mediafile/update/$', views.api_update_annotation_mediafile, name='api_update_annotation_mediafile'), + + re_path(r'^api/segmentation/(?P\d+)/tiles/(?P\d+)/(?P\d+)/(?P\d+)/$', + views.segmentation_tile, name='segmentation_tile'), ] diff --git a/exact/exact/annotations/views.py b/exact/exact/annotations/views.py index c8f07409..e7db3b05 100644 --- a/exact/exact/annotations/views.py +++ b/exact/exact/annotations/views.py @@ -1,4 +1,5 @@ import datetime +import io import time import pytz from timeit import default_timer as timer @@ -24,7 +25,7 @@ from exact.administration.models import Product from exact.annotations.forms import ExportFormatCreationForm, ExportFormatEditForm, AnnotationMediafileForm from exact.annotations.models import Annotation, AnnotationType, Export, \ - Verification, ExportFormat, LogImageAction, AnnotationMediaFile + Verification, ExportFormat, LogImageAction, AnnotationMediaFile, SegmentationTile from exact.annotations.serializers import AnnotationSerializer, AnnotationTypeSerializer, \ AnnotationSerializerFast, serialize_annotation, AnnotationMediaFileSerializer from exact.images.models import Image, ImageSet @@ -888,6 +889,7 @@ def load_set_annotations(request) -> Response: def load_annotation_types(request) -> Response: annotation_types = None + imageset = None if 'imageset_id' in request.query_params: imageset_id = int(request.query_params['imageset_id']) imageset = get_object_or_404(ImageSet, pk=imageset_id) @@ -897,7 +899,7 @@ def load_annotation_types(request) -> Response: else: annotation_types = AnnotationType.objects.filter(active=True).order_by('sort_order') - if not imageset.has_perm('read', request.user): + if imageset is not None and not imageset.has_perm('read', request.user): return Response({ 'detail': 'permission for reading this image set missing.', }, status=HTTP_403_FORBIDDEN) @@ -1154,3 +1156,100 @@ def api_blurred_concealed_annotation(request) -> Response: return Response({ 'detail': 'you updated the last annotation', }, status=HTTP_200_OK) + + +# --------------------------------------------------------------------------- +# Segmentation tile endpoints +# --------------------------------------------------------------------------- + +def _get_segmentation_annotation(annotation_id, user, require_edit=False): + """Return annotation if it exists, is a segmentation type, and user has access.""" + annotation = get_object_or_404(Annotation, pk=annotation_id) + if annotation.annotation_type.vector_type != AnnotationType.VECTOR_TYPE.SEGMENTATION: + return None, HttpResponse('Annotation is not a segmentation type.', status=400) + perm = 'edit_annotation' if require_edit else 'read' + if not annotation.image.image_set.has_perm(perm, user): + return None, HttpResponseForbidden() + return annotation, None + + +@login_required +def segmentation_tile(request, annotation_id, level, tile_x, tile_y): + """GET → return tile PNG (204 if tile does not exist yet) + PUT → merge incoming PNG onto stored tile and save + DELETE → remove tile + """ + frame = int(request.GET.get('frame', 0)) + + if request.method == 'GET': + _, err = _get_segmentation_annotation(annotation_id, request.user) + if err: + return err + try: + tile = SegmentationTile.objects.get( + annotation_id=annotation_id, + level=level, tile_x=tile_x, tile_y=tile_y, frame=frame, + ) + except SegmentationTile.DoesNotExist: + return HttpResponse(status=204) + return HttpResponse(bytes(tile.data), content_type='image/png') + + elif request.method == 'PUT': + from PIL import Image as PILImage + import numpy as np + + _, err = _get_segmentation_annotation( + annotation_id, request.user, require_edit=True) + if err: + return err + + incoming_bytes = request.body + if not incoming_bytes: + return HttpResponse('Empty body.', status=400) + + try: + incoming_img = PILImage.open(io.BytesIO(incoming_bytes)).convert('RGB') + except Exception: + return HttpResponse('Could not decode PNG.', status=400) + + incoming = np.array(incoming_img, dtype=np.uint8) + + try: + existing_tile = SegmentationTile.objects.get( + annotation_id=annotation_id, + level=level, tile_x=tile_x, tile_y=tile_y, frame=frame, + ) + except SegmentationTile.DoesNotExist: + existing_tile = None + + # Threshold red channel at 127 → clean 0/255 binary mask (L-mode PNG). + # Converting via RGB avoids PIL's RGBA→L alpha-composite-on-white behaviour + # which would corrupt transparent (unlabeled) pixels. + mask = (incoming[:, :, 0] > 127).astype(np.uint8) * 255 + buf = io.BytesIO() + PILImage.fromarray(mask, mode='L').save(buf, format='PNG') + png_bytes = buf.getvalue() + + if existing_tile is not None: + existing_tile.data = png_bytes + existing_tile.save() + else: + SegmentationTile.objects.create( + annotation_id=annotation_id, + level=level, tile_x=tile_x, tile_y=tile_y, frame=frame, + data=png_bytes, + ) + return HttpResponse(status=204) + + elif request.method == 'DELETE': + _, err = _get_segmentation_annotation( + annotation_id, request.user, require_edit=True) + if err: + return err + SegmentationTile.objects.filter( + annotation_id=annotation_id, + level=level, tile_x=tile_x, tile_y=tile_y, frame=frame, + ).delete() + return HttpResponse(status=204) + + return HttpResponse(status=405) From 2c6fbf6841ed8a52192adc0741bd6a0607b01428 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Mon, 11 May 2026 16:18:26 +0200 Subject: [PATCH 2/7] bugfix for storage of annotations. --- .../static/annotations/js/segmentationTool.js | 12 +- .../templates/annotations/annotate_v2.html | 239 ++++++++++++++++-- 2 files changed, 223 insertions(+), 28 deletions(-) diff --git a/exact/exact/annotations/static/annotations/js/segmentationTool.js b/exact/exact/annotations/static/annotations/js/segmentationTool.js index 69c513a9..599c4b97 100644 --- a/exact/exact/annotations/static/annotations/js/segmentationTool.js +++ b/exact/exact/annotations/static/annotations/js/segmentationTool.js @@ -812,11 +812,19 @@ } } catch (e) { console.error('Could not get/create annotation', e); return; } + // Re-read current frame after async fetch — user may have navigated. + const actualFrame = typeof viewer.currentPage === 'function' + ? viewer.currentPage() : frame; layer = new SegmentationLayer( - viewer, annotationId, imageWidth, imageHeight, color, frame); + viewer, annotationId, imageWidth, imageHeight, color, actualFrame); window.segmentationLayers.set(annotationTypeId, layer); } else { - if (layer.frame !== frame) layer.setFrame(frame); + // For existing layers the 'page' event keeps frame in sync; only + // force a reset if the viewer frame genuinely differs (e.g. layer was + // created on a different image navigation). + const currentFrame = typeof viewer.currentPage === 'function' + ? viewer.currentPage() : frame; + if (layer.frame !== currentFrame) layer.setFrame(currentFrame); } segmentationUI.activateType(annotationTypeId); diff --git a/exact/exact/annotations/templates/annotations/annotate_v2.html b/exact/exact/annotations/templates/annotations/annotate_v2.html index 0867a2b5..bc38e827 100644 --- a/exact/exact/annotations/templates/annotations/annotate_v2.html +++ b/exact/exact/annotations/templates/annotations/annotate_v2.html @@ -47,6 +47,7 @@ + {% endblock additional_annotation_js %} {% block bodyblock %} @@ -236,43 +237,41 @@
{{ selected_image.name }}
{% if annotation_types %}
- +
+ + - - - - + + + - + + {% for annotation_type in annotation_types %} - + + - - - + + - {% endfor %} +
LabelKey - - ColorExampleKeyCountVis
+ style="font-size:0.85rem"> {{ annotation_type.name }} + {% if annotation_type.vector_type == 8 %} + seg + {% endif %} {{ forloop.counter }} - {{ annotation_type.node_count }} + {{ forloop.counter }} - - - - + data-annotation_type_id="{{ annotation_type.id }}" checked>
{% endif %} @@ -882,6 +881,69 @@
{{ selected_image.name }}
+ + + + + {% endblock %} \ No newline at end of file From c2c9781e44363887d135494721fc30c1d019a310 Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Tue, 12 May 2026 07:02:50 +0200 Subject: [PATCH 3/7] First featuer-complete version of segmentation annotation. --- .../annotations/js/exact-image-viewer.js | 6 +++ .../static/annotations/js/segmentationTool.js | 43 +++++++++++++++---- .../templates/annotations/annotate_v2.html | 2 +- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/exact/exact/annotations/static/annotations/js/exact-image-viewer.js b/exact/exact/annotations/static/annotations/js/exact-image-viewer.js index 728337ab..74c3880d 100644 --- a/exact/exact/annotations/static/annotations/js/exact-image-viewer.js +++ b/exact/exact/annotations/static/annotations/js/exact-image-viewer.js @@ -48,6 +48,7 @@ class EXACTViewer { this.heatmapToggle = false; this.currentPlane = 0; + window.exactCurrentPlane = 0; this.mprPlanes = null; this.mprActive = false; this.mprViewers = {}; // planeIdx → { osd, canvas, nFrames, dims } @@ -781,6 +782,11 @@ class EXACTViewer { this.frameSlider.on('change', this.onFrameSliderChanged.bind(this)); } + // Expose current plane globally and notify segmentation tool before + // opening the new tile sources, so layers can be torn down cleanly. + window.exactCurrentPlane = plane; + window.dispatchEvent(new CustomEvent('exactPlaneChanged', { detail: { plane } })); + this.viewer.open(tileSources); } diff --git a/exact/exact/annotations/static/annotations/js/segmentationTool.js b/exact/exact/annotations/static/annotations/js/segmentationTool.js index 599c4b97..63cc3146 100644 --- a/exact/exact/annotations/static/annotations/js/segmentationTool.js +++ b/exact/exact/annotations/static/annotations/js/segmentationTool.js @@ -28,13 +28,14 @@ // ── SegmentationLayer ──────────────────────────────────────────────── class SegmentationLayer { - constructor(viewer, annotationId, imageWidth, imageHeight, color, frame) { + constructor(viewer, annotationId, imageWidth, imageHeight, color, frame, plane) { this.viewer = viewer; this.annotationId = annotationId; this.imageWidth = imageWidth; this.imageHeight = imageHeight; this.color = hexToRgb(color); this.frame = frame || 0; + this.plane = plane || 0; // 0=axial, 1=coronal, 2=sagittal this.opacity = 0.55; this.editable = false; @@ -225,7 +226,7 @@ this.tileCache.set(k, null); try { const r = await fetch( - `/annotations/api/segmentation/${this.annotationId}/tiles/0/${tx}/${ty}/?frame=${this.frame}`, + `/annotations/api/segmentation/${this.annotationId}/tiles/${this.plane}/${tx}/${ty}/?frame=${this.frame}`, { credentials: 'same-origin' }); if (r.status === 204) { this.tileCache.set(k, new Uint8Array(TILE_SIZE * TILE_SIZE)); @@ -576,7 +577,7 @@ const labels = this.tileCache.get(k); if (!labels) return; const blob = await this._encodePNG(labels); - const url = `/annotations/api/segmentation/${this.annotationId}/tiles/0/${tx}/${ty}/?frame=${this.frame}`; + const url = `/annotations/api/segmentation/${this.annotationId}/tiles/${this.plane}/${tx}/${ty}/?frame=${this.frame}`; await fetch(url, { method: 'PUT', credentials: 'same-origin', headers: {'Content-Type':'image/png','X-CSRFToken':csrf}, @@ -770,6 +771,21 @@ _activeViewer = window.exactOSDViewer; }); + // When the MPR plane switches the image dimensions and tile space change. + // Destroy layers for the old plane; the template re-activates the current type. + window.addEventListener('exactPlaneChanged', () => { + window.destroyAllSegmentationLayers(); + // Re-activate whatever annotation type row is currently selected. + const activeRow = document.querySelector('#statistics_table .stats-row.table-active'); + if (activeRow && typeof window.selectAnnotationType === 'function') { + // Give OSD a moment to open the new tile source before reading dimensions. + const viewer = window.exactOSDViewer; + if (viewer) { + viewer.addOnceHandler('open', () => window.selectAnnotationType(activeRow)); + } + } + }); + window.activateSegmentationLayer = async function ( annotationTypeId, color, imageId, imageWidth, imageHeight, frame ) { @@ -787,10 +803,22 @@ } _activeViewer = viewer; + // Current plane (0=axial, 1=coronal, 2=sagittal). + const plane = window.exactCurrentPlane || 0; + + // Get live image dimensions from OSD tile source — correct for the current + // plane even if the template's IMAGE_WIDTH/HEIGHT are axial-only. + const item = viewer.world.getItemAt(0); + const actualWidth = item ? Math.round(item.source.dimensions.x) : imageWidth; + const actualHeight = item ? Math.round(item.source.dimensions.y) : imageHeight; + + // Layers are keyed by annotationTypeId only — one layer per type at a time. + // Plane switches call destroyAllSegmentationLayers first, so there is never + // a stale layer from another plane in the map. let layer = window.segmentationLayers.get(annotationTypeId); if (!layer) { - // Find or create annotation on server + // Find or create annotation on server (one per annotation_type per image). let annotationId = null; try { const r = await fetch( @@ -805,7 +833,7 @@ headers: {'Content-Type':'application/json','X-CSRFToken':getCsrf()}, body: JSON.stringify({ image: imageId, annotation_type: annotationTypeId, - vector: {tile_size: TILE_SIZE, width: imageWidth, height: imageHeight}, + vector: {tile_size: TILE_SIZE, width: actualWidth, height: actualHeight}, }), }); annotationId = (await cr.json()).id; @@ -816,12 +844,11 @@ const actualFrame = typeof viewer.currentPage === 'function' ? viewer.currentPage() : frame; layer = new SegmentationLayer( - viewer, annotationId, imageWidth, imageHeight, color, actualFrame); + viewer, annotationId, actualWidth, actualHeight, color, actualFrame, plane); window.segmentationLayers.set(annotationTypeId, layer); } else { // For existing layers the 'page' event keeps frame in sync; only - // force a reset if the viewer frame genuinely differs (e.g. layer was - // created on a different image navigation). + // force a reset if the viewer frame genuinely differs. const currentFrame = typeof viewer.currentPage === 'function' ? viewer.currentPage() : frame; if (layer.frame !== currentFrame) layer.setFrame(currentFrame); diff --git a/exact/exact/annotations/templates/annotations/annotate_v2.html b/exact/exact/annotations/templates/annotations/annotate_v2.html index bc38e827..c187ae4c 100644 --- a/exact/exact/annotations/templates/annotations/annotate_v2.html +++ b/exact/exact/annotations/templates/annotations/annotate_v2.html @@ -997,7 +997,7 @@
{{ selected_image.name }}
// Keep track of the currently active row var activeRow = null; - function selectAnnotationType(row) { + window.selectAnnotationType = function selectAnnotationType(row) { // Update visual active state if (activeRow) activeRow.classList.remove('table-active'); row.classList.add('table-active'); From 1f5905a964daee3f9cc570e3d25c6d03b844c63a Mon Sep 17 00:00:00 2001 From: Marc Aubreville Date: Tue, 12 May 2026 16:33:27 +0200 Subject: [PATCH 4/7] Fix for switching between views (coronal,sagital, axial) --- .../annotations/js/exact-image-viewer.js | 21 ++- .../static/annotations/js/segmentationTool.js | 124 +++++++++++++++--- .../templates/annotations/annotate_v2.html | 46 ++++++- 3 files changed, 167 insertions(+), 24 deletions(-) diff --git a/exact/exact/annotations/static/annotations/js/exact-image-viewer.js b/exact/exact/annotations/static/annotations/js/exact-image-viewer.js index 74c3880d..a38212a7 100644 --- a/exact/exact/annotations/static/annotations/js/exact-image-viewer.js +++ b/exact/exact/annotations/static/annotations/js/exact-image-viewer.js @@ -49,6 +49,7 @@ class EXACTViewer { this.currentPlane = 0; window.exactCurrentPlane = 0; + this._planeFrameMemory = {}; // plane index → last OSD page (0-indexed) this.mprPlanes = null; this.mprActive = false; this.mprViewers = {}; // planeIdx → { osd, canvas, nFrames, dims } @@ -750,6 +751,10 @@ class EXACTViewer { switchPlane(plane) { if (!this.mprPlanes || plane === this.currentPlane) return; + + // Remember where we are in the current plane before switching. + this._planeFrameMemory[this.currentPlane] = this.viewer.currentPage(); + this.currentPlane = plane; this.updatePlaneButtons(); @@ -760,13 +765,18 @@ class EXACTViewer { const nFrames = planeInfo.nFrames; const zDim = plane + 1; + // Restore last-seen frame for this plane (0-indexed), clamped to valid range. + const restorePage = Math.min( + this._planeFrameMemory[plane] ?? 0, + nFrames - 1 + ); + const tileSources = []; for (let f = 0; f < nFrames; f++) { tileSources.push(`${this.server_url}/images/image/${this.imageId}/${zDim}/${f + 1}/tile/`); } - // Rebuild the frame slider for the new plane's frame count so the - // range and value are correct before viewer.open() fires its page event. + // Rebuild the frame slider with the restored frame position. if (this.frameSlider !== undefined) { this.frameSlider.destroy(); this.frameSlider = undefined; @@ -774,7 +784,7 @@ class EXACTViewer { if (nFrames > 1) { this.frameSlider = new Slider("#frameSlider", { ticks_snap_bounds: 1, - value: 1, + value: restorePage + 1, // slider is 1-indexed min: 0, tooltip: 'always', max: nFrames - 1 @@ -787,6 +797,11 @@ class EXACTViewer { window.exactCurrentPlane = plane; window.dispatchEvent(new CustomEvent('exactPlaneChanged', { detail: { plane } })); + // Navigate to the restored frame once OSD has opened the new tile sources. + if (restorePage > 0) { + this.viewer.addOnceHandler('open', () => this.viewer.goToPage(restorePage)); + } + this.viewer.open(tileSources); } diff --git a/exact/exact/annotations/static/annotations/js/segmentationTool.js b/exact/exact/annotations/static/annotations/js/segmentationTool.js index 63cc3146..9bd7a9d7 100644 --- a/exact/exact/annotations/static/annotations/js/segmentationTool.js +++ b/exact/exact/annotations/static/annotations/js/segmentationTool.js @@ -131,11 +131,88 @@ this.viewer.addHandler('page', this._onPage); } + // ── Cursors ────────────────────────────────────────────────────── + + static _makeSvgCursor(svgBody, hx, hy) { + const svg = `${svgBody}`; + return `url("data:image/svg+xml,${encodeURIComponent(svg)}") ${hx} ${hy}, crosshair`; + } + + static get CURSORS() { + if (!SegmentationLayer._cursors) { + const mk = SegmentationLayer._makeSvgCursor.bind(SegmentationLayer); + // Fill: paint bucket shape + const bucket = ` + + + + `; + // Wand add: wand line with + badge + const wandAdd = ` + + +`; + // Wand remove: wand line with − badge + const wandRem = ` + + `; + + SegmentationLayer._cursors = { + pencil: 'crosshair', + eraser: 'cell', + pan: 'grab', + fill: mk(bucket, 8, 22), + wand: mk(wandAdd, 3, 25), + wand_erase: mk(wandRem, 3, 25), + }; + } + return SegmentationLayer._cursors; + } + + _wandCursor() { + return this._wandEraseMode + ? SegmentationLayer.CURSORS.wand_erase + : SegmentationLayer.CURSORS.wand; + } + + _applyToolCursor() { + const C = SegmentationLayer.CURSORS; + if (this.activeTool === 'wand') { + this.drawCanvas.style.cursor = this._wandCursor(); + } else { + this.drawCanvas.style.cursor = C[this.activeTool] || 'crosshair'; + } + } + // ── Draw canvas events ─────────────────────────────────────────── _bindDrawEvents() { const c = this.drawCanvas; + // Track Ctrl/Cmd for wand erase mode + this._wandEraseMode = false; + this._onModifierDown = (e) => { + if (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') return; + if ((e.ctrlKey || e.metaKey) && e.key === 'z') { + e.preventDefault(); + this.undo(); + return; + } + const wasErase = this._wandEraseMode; + this._wandEraseMode = e.ctrlKey || e.metaKey; + if (this._wandEraseMode !== wasErase && this.activeTool === 'wand') { + this._applyToolCursor(); + } + }; + this._onModifierUp = (e) => { + const wasErase = this._wandEraseMode; + this._wandEraseMode = e.ctrlKey || e.metaKey; + if (this._wandEraseMode !== wasErase && this.activeTool === 'wand') { + this._applyToolCursor(); + } + }; + document.addEventListener('keydown', this._onModifierDown); + document.addEventListener('keyup', this._onModifierUp); + c.addEventListener('mousedown', (e) => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); @@ -145,7 +222,7 @@ if (this.activeTool === 'wand') { this._preStrokeSnap = this._snapshotTileCache(); - this._magicWand(ix, iy); + this._magicWand(ix, iy, this._wandEraseMode); this._commitUndo(); this._uploadDirtyTiles(); } else if (this.activeTool === 'fill') { @@ -183,15 +260,6 @@ this._commitUndo(); this._uploadDirtyTiles(); }); - - this._onKeyDown = (e) => { - if (e.target.nodeName === 'INPUT' || e.target.nodeName === 'TEXTAREA') return; - if ((e.ctrlKey || e.metaKey) && e.key === 'z') { - e.preventDefault(); - this.undo(); - } - }; - document.addEventListener('keydown', this._onKeyDown); } // ── Coordinates ────────────────────────────────────────────────── @@ -475,8 +543,8 @@ this._rerenderDirty(); } - _magicWand(startX, startY) { - this._imageColorBFS(startX, startY, /* skipLabeled */ false); + _magicWand(startX, startY, eraseMode) { + this._imageColorBFS(startX, startY, /* skipLabeled */ false, eraseMode); this._rerenderDirty(); } @@ -485,7 +553,7 @@ // color distance from the seed is within this.tolerance. // skipLabeled=true → skip pixels already painted with any label. - _imageColorBFS(startX, startY, skipLabeled) { + _imageColorBFS(startX, startY, skipLabeled, eraseMode) { const dc = this.viewer.drawer.canvas; if (!dc) { console.warn('Seg: OSD canvas not available'); return; } @@ -556,7 +624,7 @@ Math.abs(col[1] - refG) > tol || Math.abs(col[2] - refB) > tol) continue; - this._setPixel(cx, cy, this.activeClass); + this._setPixel(cx, cy, eraseMode ? 0 : this.activeClass); filled++; if (cx > 0 && !see(cx-1, cy)) { mark(cx-1, cy); queue.push(cx-1, cy); } @@ -566,7 +634,28 @@ } } - // ── Upload ──────────────────────────────────────────────────────── + // ── Upload / slice copy ─────────────────────────────────────────── + + async copyToAdjacentFrame(delta) { + const targetFrame = this.frame + delta; + if (targetFrame < 0) return; + const csrf = getCsrf(); + const uploads = []; + this.tileCache.forEach((labels, k) => { + if (!labels) return; + const [tx, ty] = k.split('_').map(Number); + uploads.push( + this._encodePNG(labels).then(blob => + fetch(`/annotations/api/segmentation/${this.annotationId}/tiles/${this.plane}/${tx}/${ty}/?frame=${targetFrame}`, { + method: 'PUT', credentials: 'same-origin', + headers: {'Content-Type':'image/png','X-CSRFToken':csrf}, + body: blob, + }) + ) + ); + }); + await Promise.all(uploads); + } async _uploadDirtyTiles() { const keys = Array.from(this.dirtyTiles); @@ -609,7 +698,7 @@ setTool(tool) { this.activeTool = tool; this.drawCanvas.style.display = (tool && this.editable) ? 'block' : 'none'; - this.drawCanvas.style.cursor = tool==='eraser' ? 'cell' : 'crosshair'; + this._applyToolCursor(); } setEditable(editable) { @@ -658,7 +747,8 @@ this.viewer.removeHandler('animation-finish', this._onUpdate); this.viewer.removeHandler('resize', this._onResize); this.viewer.removeHandler('page', this._onPage); - if (this._onKeyDown) document.removeEventListener('keydown', this._onKeyDown); + if (this._onModifierDown) document.removeEventListener('keydown', this._onModifierDown); + if (this._onModifierUp) document.removeEventListener('keyup', this._onModifierUp); this.displayCanvas.remove(); this.drawCanvas.remove(); } diff --git a/exact/exact/annotations/templates/annotations/annotate_v2.html b/exact/exact/annotations/templates/annotations/annotate_v2.html index c187ae4c..bd7d8199 100644 --- a/exact/exact/annotations/templates/annotations/annotate_v2.html +++ b/exact/exact/annotations/templates/annotations/annotate_v2.html @@ -895,13 +895,13 @@
{{ selected_image.name }}
+ title="Pencil (draw)">🖊 + title="Magic wand – Ctrl/Cmd: erase mode">🪄 + title="Flood fill by color similarity">🪣 + title="Eraser">🧽
@@ -943,6 +943,17 @@
{{ selected_image.name }}
+ + +