histomicsui 1.8.1.dev16__tar.gz → 1.8.1.dev41__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/PKG-INFO +1 -1
  2. histomicsui-1.8.1.dev41/docs/annotation_group_restrictions.rst +41 -0
  3. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/config_options.rst +9 -0
  4. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/DrawWidget.js +237 -2
  5. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/drawWidget.styl +22 -0
  6. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/drawWidget.pug +12 -1
  7. histomicsui-1.8.1.dev41/histomicsui/web_client/utilities/allowedGroups.js +67 -0
  8. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/body/ImageView.js +18 -2
  9. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/popover/AnnotationContextMenu.js +41 -2
  10. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/PKG-INFO +1 -1
  11. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/SOURCES.txt +3 -0
  12. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/scm_file_list.json +32 -29
  13. histomicsui-1.8.1.dev41/histomicsui.egg-info/scm_version.json +8 -0
  14. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_files/.histomicsui_config.yaml +1 -0
  15. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_web_client.py +1 -0
  16. histomicsui-1.8.1.dev41/tests/web_client_specs/allowedGroupsSpec.js +371 -0
  17. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/annotationSpec.js +125 -4
  18. histomicsui-1.8.1.dev16/histomicsui.egg-info/scm_version.json +0 -8
  19. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/.circleci/config.yml +0 -0
  20. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/.editorconfig +0 -0
  21. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/.git-blame-ignore-revs +0 -0
  22. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/.gitignore +0 -0
  23. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/.pre-commit-config.yaml +0 -0
  24. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/LICENSE +0 -0
  25. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/MANIFEST.in +0 -0
  26. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/NOTICE +0 -0
  27. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/README.rst +0 -0
  28. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/codecov.yml +0 -0
  29. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/controls.rst +0 -0
  30. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/images/cut.gif +0 -0
  31. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/images/difference.gif +0 -0
  32. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/images/intersect.gif +0 -0
  33. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/images/union.gif +0 -0
  34. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/images/xor.gif +0 -0
  35. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/docs/slicer_cli_plugins.rst +0 -0
  36. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/__init__.py +0 -0
  37. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/constants.py +0 -0
  38. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/handlers.py +0 -0
  39. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/__init__.py +0 -0
  40. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/aperio.py +0 -0
  41. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/case.py +0 -0
  42. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/cohort.py +0 -0
  43. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/image.py +0 -0
  44. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/meta.py +0 -0
  45. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/pathology.py +0 -0
  46. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/models/slide.py +0 -0
  47. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/rest/__init__.py +0 -0
  48. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/rest/aperio.py +0 -0
  49. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/rest/hui_resource.py +0 -0
  50. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/rest/image_browse_resource.py +0 -0
  51. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/rest/system.py +0 -0
  52. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/rest/tcga.py +0 -0
  53. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/app.js +0 -0
  54. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/collections/StyleCollection.js +0 -0
  55. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/collections/UserCollection.js +0 -0
  56. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/collections/index.js +0 -0
  57. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/confirmDialog.js +0 -0
  58. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/editElement.js +0 -0
  59. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/editRegionOfInterest.js +0 -0
  60. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/editStyleGroups.js +0 -0
  61. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/index.js +0 -0
  62. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/metadataPlot.js +0 -0
  63. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/openAnnotatedImage.js +0 -0
  64. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/openImage.js +0 -0
  65. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/dialogs/saveAnnotation.js +0 -0
  66. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/events.js +0 -0
  67. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/index.js +0 -0
  68. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/main.js +0 -0
  69. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/models/StyleModel.js +0 -0
  70. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/models/index.js +0 -0
  71. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/package.json +0 -0
  72. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/AnnotationSelector.js +0 -0
  73. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/FrameSelectorWidget.js +0 -0
  74. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/MetadataPlot.js +0 -0
  75. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/MetadataWidget.js +0 -0
  76. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/OverviewWidget.js +0 -0
  77. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/RegionSelector.js +0 -0
  78. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/ZoomWidget.js +0 -0
  79. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/panels/index.js +0 -0
  80. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/router.js +0 -0
  81. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/routes.js +0 -0
  82. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/static/favicon.png +0 -0
  83. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/body/configView.styl +0 -0
  84. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/body/image.styl +0 -0
  85. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/dialogs/editStyleGroups.styl +0 -0
  86. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/dialogs/metadataPlot.styl +0 -0
  87. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/dialogs/openAnnotatedImage.styl +0 -0
  88. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/dialogs/saveAnnotation.styl +0 -0
  89. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/layout/header.styl +0 -0
  90. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/layout/headerAnalyses.styl +0 -0
  91. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/layout/headerImage.styl +0 -0
  92. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/layout/layout.styl +0 -0
  93. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/annotationSelector.styl +0 -0
  94. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/frameSelectorWidget.styl +0 -0
  95. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/metadataPlot.styl +0 -0
  96. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/metadataWidget.styl +0 -0
  97. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/overviewWidget.styl +0 -0
  98. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/regionSelector.styl +0 -0
  99. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/panels/zoomWidget.styl +0 -0
  100. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/popover/annotationContextMenu.styl +0 -0
  101. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/popover/annotationPopover.styl +0 -0
  102. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/views/itemList.styl +0 -0
  103. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/stylesheets/views/searchResultsView.styl +0 -0
  104. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/body/configView.pug +0 -0
  105. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/body/image.pug +0 -0
  106. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/annotatedImageList.pug +0 -0
  107. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/confirmDialog.pug +0 -0
  108. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/editElement.pug +0 -0
  109. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/editRegionOfInterest.pug +0 -0
  110. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/editStyleGroups.pug +0 -0
  111. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/metadataPlot.pug +0 -0
  112. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/openAnnotatedImage.pug +0 -0
  113. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/openImage.pug +0 -0
  114. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/dialogs/saveAnnotation.pug +0 -0
  115. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/elementProps.pug +0 -0
  116. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/layout/header.pug +0 -0
  117. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/layout/headerAnalyses.pug +0 -0
  118. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/layout/headerImage.pug +0 -0
  119. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/layout/headerUser.pug +0 -0
  120. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/layout/layout.pug +0 -0
  121. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/annotationSelector.pug +0 -0
  122. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/drawWidgetElement.pug +0 -0
  123. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/frameSelectorWidget.pug +0 -0
  124. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/metadataPlot.pug +0 -0
  125. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/metadataWidgetPanel.pug +0 -0
  126. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/overviewWidget.pug +0 -0
  127. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/panel.pug +0 -0
  128. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/regionSelector.pug +0 -0
  129. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/panels/zoomWidget.pug +0 -0
  130. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/popover/annotationContextMenu.pug +0 -0
  131. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/popover/annotationPopover.pug +0 -0
  132. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/popover/annotationPopoverMetadata.pug +0 -0
  133. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/templates/popover/pixelmapContextMenu.pug +0 -0
  134. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/HierarchyWidget.js +0 -0
  135. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/JobDetailsWidget.js +0 -0
  136. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/View.js +0 -0
  137. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/body/ConfigView.js +0 -0
  138. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/body/index.js +0 -0
  139. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/index.js +0 -0
  140. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/itemList.js +0 -0
  141. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/itemPage.js +0 -0
  142. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/layout/HeaderAnalysesView.js +0 -0
  143. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/layout/HeaderImageView.js +0 -0
  144. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/layout/HeaderUserView.js +0 -0
  145. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/layout/HeaderView.js +0 -0
  146. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/layout/LoginView.js +0 -0
  147. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/layout/index.js +0 -0
  148. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/popover/AnnotationPopover.js +0 -0
  149. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/popover/PixelmapContextMenu.js +0 -0
  150. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/popover/index.js +0 -0
  151. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/searchResultsView.js +0 -0
  152. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/views/utils.js +0 -0
  153. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/vue/components/AnnotationHistoryBrowser.vue +0 -0
  154. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/vue/components/AnnotationHistoryBrowserContainer.vue +0 -0
  155. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/vue/components/AnnotationHistoryGroup.vue +0 -0
  156. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/vue/components/ColorPickerInput.vue +0 -0
  157. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/vue/components/EditHeatmapOrGridData.vue +0 -0
  158. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/vue/components/EditHeatmapOrGridDataContainer.vue +0 -0
  159. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/web_client/webpack.helper.js +0 -0
  160. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui/webroot.mako +0 -0
  161. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/dependency_links.txt +0 -0
  162. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/entry_points.txt +0 -0
  163. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/not-zip-safe +0 -0
  164. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/requires.txt +0 -0
  165. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/histomicsui.egg-info/top_level.txt +0 -0
  166. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/pyproject.toml +0 -0
  167. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/requirements-dev.txt +0 -0
  168. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/ruff.toml +0 -0
  169. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/setup.cfg +0 -0
  170. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/setup.py +0 -0
  171. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/__init__.py +0 -0
  172. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/datastore.py +0 -0
  173. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/girder_utilities.py +0 -0
  174. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_files/sample.anot +0 -0
  175. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_files/sample.meta +0 -0
  176. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_files/sample_girder_id.anot +0 -0
  177. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_files/test_analysis_detection.xml +0 -0
  178. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_files/test_analysis_features.xml +0 -0
  179. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_handlers.py +0 -0
  180. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_hui_rest.py +0 -0
  181. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_image_browse_endpoints.py +0 -0
  182. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_load.py +0 -0
  183. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/test_tcga.py +0 -0
  184. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/.eslintrc +0 -0
  185. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/analysisSpec.js +0 -0
  186. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/girderUISpec.js +0 -0
  187. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/huiSpec.js +0 -0
  188. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/huiTest.js +0 -0
  189. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/itemSpec.js +0 -0
  190. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/metadataPanelSpec.js +0 -0
  191. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/metadataPlotSpec.js +0 -0
  192. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/overviewPanelSpec.js +0 -0
  193. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/panelLayoutSpec.js +0 -0
  194. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tests/web_client_specs/pixelmapCategorySpec.js +0 -0
  195. {histomicsui-1.8.1.dev16 → histomicsui-1.8.1.dev41}/tox.ini +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: histomicsui
3
- Version: 1.8.1.dev16
3
+ Version: 1.8.1.dev41
4
4
  Summary: Organize, visualize, and analyze histology images.
5
5
  Home-page: https://github.com/DigitalSlideArchive/histomicsui
6
6
  Author: Kitware, Inc.
@@ -0,0 +1,41 @@
1
+ Restricting Annotations to Allowed Groups
2
+ ==========================================
3
+
4
+ An annotation document can restrict which style groups its elements may be assigned to by setting an ``allowed_groups`` key in the annotation's ``attributes``:
5
+
6
+ ::
7
+
8
+ {
9
+ "name": "restricted annotation",
10
+ "attributes": {
11
+ "allowed_groups": ["groupA", "groupB"]
12
+ },
13
+ "elements": [...]
14
+ }
15
+
16
+ ``allowed_groups`` may also be given as a JSON-stringified array for convenience when setting it through the UI metadata editor:
17
+
18
+ ::
19
+
20
+ {
21
+ "attributes": {
22
+ "allowed_groups": "[\"groupA\", \"groupB\"]"
23
+ }
24
+ }
25
+
26
+ When this restriction is present, the Draw panel and the annotation element context menu will only offer the listed groups instead of every style group that exists.
27
+
28
+ Validation
29
+ ----------
30
+
31
+ ``allowed_groups`` is only honored when it is a non-empty array of non-empty strings, or a string that parses as JSON into such an array. Any other value - a missing key, a non-array/non-JSON-array value, an unparseable string, an empty array, or an array containing no valid strings - is treated as *unrestricted*, and every existing style group is offered as usual. Duplicate entries are ignored.
32
+
33
+ Auto-creation of missing groups
34
+ --------------------------------
35
+
36
+ If ``allowed_groups`` names a style group that does not yet exist, it is automatically created the first time the restriction is encountered, by copying the style (fill color, line color, line width, pattern) of the default group. The newly created groups are persisted immediately. This can happen from either the Draw panel or the annotation context menu, whichever sees the restricted annotation first.
37
+
38
+ Effect on the current selection
39
+ --------------------------------
40
+
41
+ - **Draw panel**: If the currently active style group is not one of the annotation's allowed groups, the Draw panel automatically switches to the first allowed group that exists.
@@ -79,6 +79,15 @@ If a hotkey is specified for a group, it will only be active if the group was lo
79
79
  action: group
80
80
  param: Red
81
81
 
82
+ Unselected Annotation Capacity
83
+ ______________________________
84
+ When annotation elements are selected, the annotations not included in the user's selection have their opacity decreased to 0.33 times their specified opacity. This can be modified:
85
+ ::
86
+ ---
87
+ # A lower value will make unselected annotations more transparent.
88
+ unselectedOpacityMultiplier: 0.33
89
+
90
+
82
91
  UI Settings
83
92
  ___________
84
93
 
@@ -15,6 +15,7 @@ import StyleCollection from '../collections/StyleCollection';
15
15
  import StyleModel from '../models/StyleModel';
16
16
  import editElement from '../dialogs/editElement';
17
17
  import editStyleGroups from '../dialogs/editStyleGroups';
18
+ import getAllowedGroups, {ensureAllowedGroupsExist} from '../utilities/allowedGroups';
18
19
  import drawWidget from '../templates/panels/drawWidget.pug';
19
20
  import drawWidgetElement from '../templates/panels/drawWidgetElement.pug';
20
21
  import '../stylesheets/panels/drawWidget.styl';
@@ -31,6 +32,8 @@ var DrawWidget = Panel.extend({
31
32
  'click .h-draw': 'drawElement',
32
33
  'click .h-group-count-option .h-group-count-select': 'selectElementsInGroup',
33
34
  'change .h-style-group': '_setToSelectedStyleGroup',
35
+ 'change .h-sort-mode': '_changeSortMode',
36
+ 'click .h-sort-order': '_toggleSortOrder',
34
37
  'change .h-brush-shape,.h-brush-size,.h-brush-screen': '_changeBrush',
35
38
  'change .h-fixed-shape,.h-fixed-height,.h-fixed-width': '_changeShapeConstraint',
36
39
  'click .h-configure-style-group': '_styleGroupEditor',
@@ -64,9 +67,12 @@ var DrawWidget = Panel.extend({
64
67
  this._groups = new StyleCollection();
65
68
  this._style = new StyleModel({id: this.parentView._defaultGroup});
66
69
  this.listenTo(this._groups, 'add change', this._handleStyleGroupsUpdate);
67
- this.listenTo(this._groups, 'remove', this.render);
70
+ this.listenTo(this._groups, 'remove', this._handleStyleGroupsRemoved);
68
71
  this.listenTo(this.collection, 'add remove reset', this._recalculateGroupAggregation);
69
72
  this.listenTo(this.collection, 'change update reset', this.render);
73
+ // if the annotation's metadata is edited while it is active, react immediately
74
+ this._lastAllowedGroupsKey = this._allowedGroupsKey();
75
+ this.listenTo(this.annotation, 'change:annotation', this._handleAnnotationAttributesChange);
70
76
  this._groups.fetch().done(() => {
71
77
  // ensure the default style exists
72
78
  if (this._groups.has(this.parentView._defaultGroup)) {
@@ -75,9 +81,12 @@ var DrawWidget = Panel.extend({
75
81
  this._groups.add(this._style.toJSON());
76
82
  this._groups.get(this._style.id).save();
77
83
  }
84
+ this._ensureAllowedGroupsExist();
78
85
  if (this._editOptions.style && this._groups.get(this._editOptions.style)) {
79
86
  this._setStyleGroup(this._groups.get(this._editOptions.style).toJSON());
80
87
  }
88
+ this._restrictStyleToAllowedGroups();
89
+ this._debounceRender();
81
90
  });
82
91
  this.on('h:mouseon', (model) => {
83
92
  if (model && model.id) {
@@ -110,10 +119,11 @@ var DrawWidget = Panel.extend({
110
119
  delete this._skipRenderHTML;
111
120
  }
112
121
  } else {
122
+ this._sortElements();
113
123
  this.$el.html(drawWidget({
114
124
  title: 'Draw',
115
125
  elements: this.collection.models,
116
- groups: this._groups,
126
+ groups: this._groupsForDisplay(),
117
127
  style: this._style.id,
118
128
  defaultGroup: this.parentView._defaultGroup,
119
129
  highlighted: this._highlighted,
@@ -123,6 +133,8 @@ var DrawWidget = Panel.extend({
123
133
  collapsed: this.$('.s-panel-content.collapse').length && !this.$('.s-panel-content').hasClass('in'),
124
134
  firstRender: true,
125
135
  displayIdStart: 0,
136
+ sortMode: this._editOptions.sort_mode || 'label',
137
+ sortOrder: this._editOptions.sort_order || 'asc',
126
138
  partialCount: this.annotation && this.annotation._pageElements
127
139
  }));
128
140
  this.$('.h-dropdown-content').collapse({toggle: false});
@@ -372,6 +384,7 @@ var DrawWidget = Panel.extend({
372
384
  }
373
385
  }
374
386
  }
387
+ this._reorderElementDom();
375
388
  },
376
389
 
377
390
  /**
@@ -818,6 +831,17 @@ var DrawWidget = Panel.extend({
818
831
  if (!opts.size_mode) {
819
832
  opts.size_mode = 'unconstrained';
820
833
  }
834
+ if (opts.sort_mode === 'label-reverse') {
835
+ // migrate the legacy combined mode/order value
836
+ opts.sort_mode = 'label';
837
+ opts.sort_order = 'desc';
838
+ }
839
+ if (!opts.sort_mode || !['label', 'group', 'shape', 'count'].includes(opts.sort_mode)) {
840
+ opts.sort_mode = 'label';
841
+ }
842
+ if (!opts.sort_order || !['asc', 'desc'].includes(opts.sort_order)) {
843
+ opts.sort_order = 'asc';
844
+ }
821
845
  },
822
846
 
823
847
  updateCount(groupName, change) {
@@ -1071,10 +1095,99 @@ var DrawWidget = Panel.extend({
1071
1095
  },
1072
1096
 
1073
1097
  _handleStyleGroupsUpdate() {
1098
+ this._restrictStyleToAllowedGroups();
1074
1099
  this._debounceRender();
1075
1100
  this.trigger('h:styleGroupsUpdated', this._groups);
1076
1101
  },
1077
1102
 
1103
+ _handleStyleGroupsRemoved() {
1104
+ this._restrictStyleToAllowedGroups();
1105
+ this.render();
1106
+ },
1107
+
1108
+ /**
1109
+ * Get the current annotation's `allowed_groups` metadata, if any.
1110
+ *
1111
+ * @returns {string[]|null} The list of allowed group names, or null if the current annotation
1112
+ * has no valid restriction.
1113
+ */
1114
+ _getAllowedGroups() {
1115
+ return getAllowedGroups(this.annotation);
1116
+ },
1117
+
1118
+ /**
1119
+ * Compute a stable, order-independent key for the current `allowed_groups` restriction, used
1120
+ * to detect whether it actually changed. Returns an empty string for "unrestricted".
1121
+ *
1122
+ * @returns {string} The restriction key.
1123
+ */
1124
+ _allowedGroupsKey() {
1125
+ const allowed = this._getAllowedGroups();
1126
+ return allowed ? allowed.slice().sort().join('\u0000') : '';
1127
+ },
1128
+
1129
+ /**
1130
+ * Respond to the active annotation's metadata being edited, which may have changed its
1131
+ * `allowed_groups` restriction. The annotation's `change:annotation` event also fires on
1132
+ * every autosave refetch, regardless of whether `allowed_groups` changed, so this bails out
1133
+ * early when the restriction is unchanged to avoid re-rendering on unrelated edits.
1134
+ */
1135
+ _handleAnnotationAttributesChange() {
1136
+ const key = this._allowedGroupsKey();
1137
+ if (key === this._lastAllowedGroupsKey) {
1138
+ return;
1139
+ }
1140
+ this._lastAllowedGroupsKey = key;
1141
+ this._ensureAllowedGroupsExist();
1142
+ this._restrictStyleToAllowedGroups();
1143
+ this._debounceRender();
1144
+ },
1145
+
1146
+ /**
1147
+ * If the current annotation restricts its elements to a set of allowed_groups, create any of
1148
+ * those groups that don't already exist, copying the current default group's style.
1149
+ */
1150
+ _ensureAllowedGroupsExist() {
1151
+ const saves = ensureAllowedGroupsExist(
1152
+ this._groups, this._getAllowedGroups(), this.parentView._defaultGroup);
1153
+ if (!saves.length) {
1154
+ return;
1155
+ }
1156
+ // Let other views know new groups exist after they're persisted so that a page refresh is
1157
+ // not needed.
1158
+ $.when(...saves).done(() => {
1159
+ this.parentView.trigger('h:styleGroupsEdited', this._groups);
1160
+ });
1161
+ },
1162
+
1163
+ /**
1164
+ * Return the style groups that should be offered to the user given the current annotation's
1165
+ * `allowed_groups` restriction, if any, sorted alphabetically by id.
1166
+ *
1167
+ * @returns {object[]} A list of plain style group attribute objects.
1168
+ */
1169
+ _groupsForDisplay() {
1170
+ const allowed = this._getAllowedGroups();
1171
+ const groups = allowed ? this._groups.filter((group) => allowed.includes(group.id)) : this._groups.models;
1172
+ return _.sortBy(groups, 'id').map((group) => group.toJSON());
1173
+ },
1174
+
1175
+ /**
1176
+ * If the current annotation restricts its elements to a set of `allowed_groups` and the
1177
+ * currently selected style is not one of them, switch to the first allowed group that exists.
1178
+ */
1179
+ _restrictStyleToAllowedGroups() {
1180
+ const allowed = this._getAllowedGroups();
1181
+ if (!allowed || allowed.includes(this._style.id)) return;
1182
+
1183
+ const candidates = this._groups.filter((group) => allowed.includes(group.id))
1184
+ .map((group) => group.id)
1185
+ .sort();
1186
+ if (candidates.length) {
1187
+ this._setStyleGroup(this._groups.get(candidates[0]).toJSON());
1188
+ }
1189
+ },
1190
+
1078
1191
  _highlightElement(evt) {
1079
1192
  const id = $(evt.currentTarget).data('id');
1080
1193
  const annotType = this.collection._byId[id].get('type');
@@ -1089,6 +1202,128 @@ var DrawWidget = Panel.extend({
1089
1202
  this.parentView.trigger('h:highlightAnnotation');
1090
1203
  },
1091
1204
 
1205
+ /**
1206
+ * Resolve the displayed shape name of an element, matching the label logic in
1207
+ * drawWidgetElement.pug (closed polylines are polygons, open ones are lines).
1208
+ *
1209
+ * @param {ElementModel} model The element to inspect.
1210
+ * @returns {string} The shape name.
1211
+ */
1212
+ _elementShape(model) {
1213
+ const element = model.attributes;
1214
+ return element.type === 'polyline'
1215
+ ? (element.closed ? 'polygon' : 'line')
1216
+ : element.type;
1217
+ },
1218
+
1219
+ /**
1220
+ * Resolve the group name of an element, falling back to the default group.
1221
+ *
1222
+ * @param {ElementModel} model The element to inspect.
1223
+ * @returns {string} The group name.
1224
+ */
1225
+ _elementGroupName(model) {
1226
+ return model.attributes.group || this.parentView._defaultGroup;
1227
+ },
1228
+
1229
+ /**
1230
+ * Compute a lexical sort key for an element that matches its displayed label. The number is
1231
+ * intentionally omitted so that elements sort by their base label before enumeration.
1232
+ *
1233
+ * @param {ElementModel} model The element to compute a key for.
1234
+ * @returns {string} A lower-case sort key.
1235
+ */
1236
+ _elementSortKey(model) {
1237
+ const element = model.attributes;
1238
+ const userLabel = (element.label || {}).value;
1239
+ if (userLabel) {
1240
+ return ('' + userLabel).toLowerCase();
1241
+ }
1242
+ const shape = this._elementShape(model);
1243
+ if (['point', 'polyline', 'rectangle', 'ellipse', 'circle'].includes(element.type)) {
1244
+ return `${this._elementGroupName(model)} ${shape}`.toLowerCase();
1245
+ }
1246
+ return ('' + shape).toLowerCase();
1247
+ },
1248
+
1249
+ /**
1250
+ * Count how many elements in the current collection belong to each group.
1251
+ *
1252
+ * @returns {Object} A map of group name to element count.
1253
+ */
1254
+ _elementGroupCounts() {
1255
+ const counts = {};
1256
+ this.collection.models.forEach((model) => {
1257
+ const group = this._elementGroupName(model);
1258
+ counts[group] = (counts[group] || 0) + 1;
1259
+ });
1260
+ return counts;
1261
+ },
1262
+
1263
+ /**
1264
+ * Sort the element collection's models in place according to the current sort mode.
1265
+ */
1266
+ _sortElements() {
1267
+ const groupCounts = this._elementGroupCounts();
1268
+ const comparators = {
1269
+ label: (elementA, elementB) => this._elementSortKey(elementA).localeCompare(this._elementSortKey(elementB)),
1270
+ group: (elementA, elementB) => this._elementGroupName(elementA).toLowerCase().localeCompare(this._elementGroupName(elementB).toLowerCase()),
1271
+ shape: (elementA, elementB) => this._elementShape(elementA).toLowerCase().localeCompare(this._elementShape(elementB).toLowerCase()),
1272
+ count: (elementA, elementB) => {
1273
+ const groupA = this._elementGroupName(elementA);
1274
+ const groupB = this._elementGroupName(elementB);
1275
+ const countDiff = groupCounts[groupA] - groupCounts[groupB];
1276
+ if (countDiff !== 0) {
1277
+ return countDiff;
1278
+ }
1279
+ return groupA.toLowerCase().localeCompare(groupB.toLowerCase());
1280
+ }
1281
+ };
1282
+ const comparator = comparators[this._editOptions.sort_mode] || comparators.label;
1283
+ const ordered = this._editOptions.sort_order === 'desc'
1284
+ ? (elementA, elementB) => -comparator(elementA, elementB)
1285
+ : comparator;
1286
+ this.collection.models.sort(ordered);
1287
+ },
1288
+
1289
+ /**
1290
+ * Reorder the already-rendered element rows to match the current sort without rebuilding the
1291
+ * list. Auto-assigned enumeration numbers are left as-is here and are recomputed on the next
1292
+ * full render.
1293
+ */
1294
+ _reorderElementDom() {
1295
+ const container = this.$el.find('.h-elements-container');
1296
+ if (!container.length) {
1297
+ return;
1298
+ }
1299
+ this._sortElements();
1300
+ const rowsById = {};
1301
+ container.children('.h-element').each((index, node) => {
1302
+ rowsById[$(node).attr('data-id')] = node;
1303
+ });
1304
+ this.collection.models.forEach((model) => {
1305
+ const node = rowsById[model.id];
1306
+ if (node) {
1307
+ container.append(node);
1308
+ }
1309
+ });
1310
+ },
1311
+
1312
+ _changeSortMode() {
1313
+ this._saveEditOptions({sort_mode: this.$('.h-sort-mode').val()});
1314
+ this.render();
1315
+ },
1316
+
1317
+ /**
1318
+ * Toggle between ascending and descending order for the current sort
1319
+ * mode, persist the choice, and re-render.
1320
+ */
1321
+ _toggleSortOrder() {
1322
+ const order = this._editOptions.sort_order === 'desc' ? 'asc' : 'desc';
1323
+ this._saveEditOptions({sort_order: order});
1324
+ this.render();
1325
+ },
1326
+
1092
1327
  _recalculateGroupAggregation() {
1093
1328
  const groups = [];
1094
1329
  const used = {};
@@ -97,6 +97,28 @@
97
97
  flex-shrink 0
98
98
  padding-left 6px
99
99
 
100
+ .h-sort-row
101
+ display flex
102
+ flex-direction row
103
+ align-items center
104
+ width 100%
105
+ margin 10px 0
106
+
107
+ .h-sort-label
108
+ flex-shrink 0
109
+ margin 0 6px 0 0
110
+ font-weight bold
111
+
112
+ .h-sort-mode
113
+ flex-grow 1
114
+ flex-shrink 1
115
+ width auto
116
+
117
+ .h-sort-order
118
+ flex-shrink 0
119
+ margin-left 6px
120
+ cursor pointer
121
+
100
122
  .btn-default.active
101
123
  background-color #5790ff
102
124
 
@@ -6,7 +6,7 @@ block title
6
6
  block content
7
7
  .input-group.input-group-sm.h-style-group-row
8
8
  select.form-control.h-style-group
9
- each group in groups.sortBy('id')
9
+ each group in groups
10
10
  option(value=group.id, selected=group.id === style)
11
11
  = group.id
12
12
  .input-group-btn
@@ -103,6 +103,17 @@ block content
103
103
  label(title="If checked, the size is in screen pixels. If unchecked, the size is in base image pixels")
104
104
  input.h-brush-screen(type="checkbox", checked=opts.brush_screen ? 'checked' : undefined)
105
105
  | Screen
106
+ .h-sort-row
107
+ label.h-sort-label(for='h-sort-mode') Sort By
108
+ select#h-sort-mode.h-sort-mode.form-control.input-sm(title='Sort annotation elements')
109
+ option(value='label', selected=sortMode === 'label') Label
110
+ option(value='group', selected=sortMode === 'group') Group
111
+ option(value='shape', selected=sortMode === 'shape') Shape
112
+ option(value='count', selected=sortMode === 'count') Count
113
+ a.h-sort-order(
114
+ href='javascript:void(0)',
115
+ title=sortOrder === 'desc' ? 'Sort descending (click to sort ascending)' : 'Sort ascending (click to sort descending)')
116
+ i(class=sortOrder === 'desc' ? 'icon-sort-alt-down' : 'icon-sort-alt-up')
106
117
  .h-group-count
107
118
  if partialCount
108
119
  b.h-group-count-label(title='* These counts only apply to loaded elements') Count*:
@@ -0,0 +1,67 @@
1
+ import _ from 'underscore';
2
+
3
+ /**
4
+ * Read and validate the `allowed_groups` metadata on an annotation.
5
+ *
6
+ * The value is expected to live at `annotation.get('annotation').attributes.allowed_groups`
7
+ * and be an array of strings. As a convenience for the UI metadata editor, a JSON-stringified
8
+ * array (e.g. `'["groupA", "groupB"]'`) is also accepted and parsed. Any other value (missing,
9
+ * not an array or JSON array string, empty array, unparseable string, etc.) is treated as
10
+ * "unrestricted" and returns `null`.
11
+ *
12
+ * @param {AnnotationModel} annotation The annotation to check.
13
+ * @returns {string[]|null} The list of allowed group names, or null if there are no restrictions.
14
+ */
15
+ function getAllowedGroups(annotation) {
16
+ if (!annotation) return null;
17
+
18
+ const attributes = (annotation.get('annotation') || {}).attributes || {};
19
+ let allowedGroups = attributes.allowed_groups;
20
+
21
+ if (_.isString(allowedGroups)) {
22
+ try {
23
+ allowedGroups = JSON.parse(allowedGroups);
24
+ } catch (err) {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ if (!_.isArray(allowedGroups)) return null;
30
+
31
+ const filtered = _.uniq(allowedGroups.filter((group) => _.isString(group) && group.length));
32
+ return filtered.length ? filtered : null;
33
+ }
34
+
35
+ /**
36
+ * Ensure that every group in an `allowed_groups` restriction exists as a persisted style group.
37
+ * Create any that are missing by copying the style of the default group.
38
+ *
39
+ * Newly created groups are added to the collection synchronously and each is persisted
40
+ * asynchronously. The caller is responsible for reacting to the returned save promises.
41
+ *
42
+ * @param {StyleCollection} styles The style-group collection to populate.
43
+ * @param {string[]|null} allowed The validated `allowed_groups` restriction, or `null` for
44
+ * "unrestricted" (in which case nothing is created).
45
+ * @param {string} defaultGroupId The id of the default style group to copy.
46
+ * @returns {Array} The list of save promises for the newly created groups.
47
+ */
48
+ function ensureAllowedGroupsExist(styles, allowed, defaultGroupId) {
49
+ if (!allowed) {
50
+ return [];
51
+ }
52
+ const missing = allowed.filter((groupId) => !styles.get(groupId));
53
+ if (!missing.length) {
54
+ return [];
55
+ }
56
+ // we assume the default group always exists; if it somehow does not, new groups are created
57
+ // with no inherited style rather than failing
58
+ const defaultGroup = styles.get(defaultGroupId);
59
+ const baseAttributes = defaultGroup ? _.omit(defaultGroup.toJSON(), 'id', 'group') : {};
60
+ return missing.map((groupId) => {
61
+ styles.add(Object.assign({}, baseAttributes, {id: groupId}));
62
+ return styles.get(groupId).save();
63
+ });
64
+ }
65
+
66
+ export default getAllowedGroups;
67
+ export {ensureAllowedGroupsExist};
@@ -205,13 +205,17 @@ var ImageView = View.extend({
205
205
  // it is very confusing if this value is smaller than the
206
206
  // AnnotationSelector MAX_ELEMENTS_LIST_LENGTH
207
207
  highlightFeatureSizeLimit: 5000,
208
- scale: {position: {bottom: 20, right: 10}}
208
+ scale: {position: {bottom: 20, right: 10}},
209
+ unselectedOpacityMultiplier: typeof this._unselectedOpacityMultiplier !== 'number'
210
+ ? 0.33
211
+ : this._unselectedOpacityMultiplier
209
212
  });
210
213
  // Don't unclamp bounds for the image even if image overlays are present.
211
214
  if (this.viewerWidget.setUnclampBoundsForOverlay) {
212
215
  this.viewerWidget.setUnclampBoundsForOverlay(false);
213
216
  }
214
217
  this.trigger('h:viewerWidgetCreated', this.viewerWidget);
218
+ this.viewerWidget.setUnselectedOpacityMultiplier(this._unselectedOpacityMultiplier);
215
219
 
216
220
  // handle annotation mouse events
217
221
  this.listenTo(this.viewerWidget, 'g:mouseOverAnnotation', this.mouseOverAnnotation);
@@ -1553,8 +1557,12 @@ var ImageView = View.extend({
1553
1557
  return;
1554
1558
  }
1555
1559
 
1556
- var elementModel = this.selectedElements.add(element.attributes, options);
1560
+ // Assign `originalAnnotation` before the element enters the collection. Setting it after
1561
+ // `add` would leave that first render seeing an undefined reference and falling back to
1562
+ // the panel's active annotation instead of the clicked one.
1563
+ var elementModel = new this.selectedElements.model(element.attributes);
1557
1564
  elementModel.originalAnnotation = annotation;
1565
+ this.selectedElements.add(elementModel, options);
1558
1566
  this.viewerWidget.highlightAnnotation(this.selectedAnnotation.id);
1559
1567
  },
1560
1568
 
@@ -1784,6 +1792,14 @@ var ImageView = View.extend({
1784
1792
  }
1785
1793
  });
1786
1794
  }
1795
+ if (val.unselectedOpacityMultiplier !== undefined) {
1796
+ this._unselectedOpacityMultiplier = typeof val.unselectedOpacityMultiplier !== 'number'
1797
+ ? 0.33
1798
+ : val.unselectedOpacityMultiplier;
1799
+ if (this.viewerWidget) {
1800
+ this.viewerWidget.setUnselectedOpacityMultiplier(val.unselectedOpacityMultiplier);
1801
+ }
1802
+ }
1787
1803
  });
1788
1804
  },
1789
1805
 
@@ -1,6 +1,7 @@
1
1
  import $ from 'jquery';
2
2
 
3
3
  import StyleCollection from '../../collections/StyleCollection';
4
+ import getAllowedGroups, {ensureAllowedGroupsExist} from '../../utilities/allowedGroups';
4
5
  import View from '../View';
5
6
 
6
7
  import template from '../../templates/popover/annotationContextMenu.pug';
@@ -19,6 +20,8 @@ const AnnotationContextMenu = View.extend({
19
20
  this.styles = new StyleCollection();
20
21
  this.styles.fetch().done(() => this.render());
21
22
  this.listenTo(this.collection, 'add remove reset', this.render);
23
+ // react immediately if any annotation's metadata is edited
24
+ this.listenTo(this.parentView.annotations, 'change:annotation', this.render);
22
25
  },
23
26
  render() {
24
27
  this.$el.html(template({
@@ -28,7 +31,16 @@ const AnnotationContextMenu = View.extend({
28
31
  return this;
29
32
  },
30
33
  refetchStyles() {
31
- this.styles.fetch().done(() => this.render());
34
+ // Prevent race conditions when multiple fetches happen in quick succession.
35
+ const requestId = (this._styleFetchRequestId = (this._styleFetchRequestId || 0) + 1);
36
+ this.styles.fetch({
37
+ success: (collection, resp, options) => {
38
+ if (requestId === this._styleFetchRequestId) {
39
+ collection.set(resp, options);
40
+ this.render();
41
+ }
42
+ }
43
+ });
32
44
  },
33
45
  setGroupCount(groupCount) {
34
46
  this._cachedGroupCount = groupCount;
@@ -96,7 +108,16 @@ const AnnotationContextMenu = View.extend({
96
108
  }
97
109
  },
98
110
  _getAnnotationGroups() {
99
- const groups = this.styles.map((style) => style.id);
111
+ // restrict to the allowed groups of the annotation that owns the selected/right-clicked
112
+ // element, not whichever annotation happens to be active in the Annotations panel
113
+ const referenceElement = this.collection.at(0);
114
+ const referenceAnnotation = (referenceElement && referenceElement.originalAnnotation) || this.parentView.activeAnnotation;
115
+ const allowed = getAllowedGroups(referenceAnnotation);
116
+ this._ensureAllowedGroupsExist(allowed);
117
+ let groups = this.styles.map((style) => style.id);
118
+ if (allowed) {
119
+ groups = groups.filter((groupId) => allowed.includes(groupId));
120
+ }
100
121
  groups.sort((a, b) => {
101
122
  const countA = this._cachedGroupCount[a] || 0;
102
123
  const countB = this._cachedGroupCount[b] || 0;
@@ -113,6 +134,24 @@ const AnnotationContextMenu = View.extend({
113
134
  });
114
135
  return groups;
115
136
  },
137
+ /**
138
+ * Create any style groups required by the given `allowed_groups` restriction that don't
139
+ * already exist. Once the new groups are persisted, notify the other views so their style
140
+ * collections stay in sync.
141
+ *
142
+ * @param {string[]|null} allowed The validated `allowed_groups` restriction, or `null` when
143
+ * unrestricted.
144
+ */
145
+ _ensureAllowedGroupsExist(allowed) {
146
+ const saves = ensureAllowedGroupsExist(
147
+ this.styles, allowed, this.parentView._defaultGroup);
148
+ if (!saves.length) {
149
+ return;
150
+ }
151
+ $.when(...saves).done(() => {
152
+ this.parentView.trigger('h:styleGroupsEdited', this.styles);
153
+ });
154
+ },
116
155
  _setGroup(evt) {
117
156
  evt.preventDefault();
118
157
  evt.stopPropagation();
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: histomicsui
3
- Version: 1.8.1.dev16
3
+ Version: 1.8.1.dev41
4
4
  Summary: Organize, visualize, and analyze histology images.
5
5
  Home-page: https://github.com/DigitalSlideArchive/histomicsui
6
6
  Author: Kitware, Inc.
@@ -14,6 +14,7 @@ setup.cfg
14
14
  setup.py
15
15
  tox.ini
16
16
  .circleci/config.yml
17
+ docs/annotation_group_restrictions.rst
17
18
  docs/config_options.rst
18
19
  docs/controls.rst
19
20
  docs/slicer_cli_plugins.rst
@@ -134,6 +135,7 @@ histomicsui/web_client/templates/popover/annotationContextMenu.pug
134
135
  histomicsui/web_client/templates/popover/annotationPopover.pug
135
136
  histomicsui/web_client/templates/popover/annotationPopoverMetadata.pug
136
137
  histomicsui/web_client/templates/popover/pixelmapContextMenu.pug
138
+ histomicsui/web_client/utilities/allowedGroups.js
137
139
  histomicsui/web_client/views/HierarchyWidget.js
138
140
  histomicsui/web_client/views/JobDetailsWidget.js
139
141
  histomicsui/web_client/views/View.js
@@ -177,6 +179,7 @@ tests/test_files/sample_girder_id.anot
177
179
  tests/test_files/test_analysis_detection.xml
178
180
  tests/test_files/test_analysis_features.xml
179
181
  tests/web_client_specs/.eslintrc
182
+ tests/web_client_specs/allowedGroupsSpec.js
180
183
  tests/web_client_specs/analysisSpec.js
181
184
  tests/web_client_specs/annotationSpec.js
182
185
  tests/web_client_specs/girderUISpec.js