qiprofile
Version:
Quantitative Imaging Profile ( QiPr) web application
279 lines (251 loc) • 11.7 kB
text/coffeescript
define ['angular', 'lodash', 'underscore.string', 'spin', 'nouislider', 'helpers',
'collection', 'timeline', 'intensityChart', 'modeling', 'SliceDisplay'],
(ng, _, _s, Spinner, noUiSlider) ->
directives = ng.module(
'qiprofile.directives',
['qiprofile.helpers', 'qiprofile.collection', 'qiprofile.timeline',
'qiprofile.intensitychart', 'qiprofile.modeling', 'qiprofile.slicedisplay']
)
# Spinner directive.
directives.directive 'qiSpin', ->
(scope, element, attrs) ->
# Replaces the DOM element with a spinner
# while the qiSpin attribute is set.
createSpinner = ->
# Creates an image selection spinner.
# position relative places the spinner element
# with respect to the button. top .6em centers
# the spinner on the button. The spinner has
# 10 spokes radiating out 2 pixels from the
# center of the button with length and width
# 4 pixels.
new Spinner(
position: 'relative'
top: '.6em'
lines: 10
width: 4
length: 4
radius: 2
trail: 80
speed: 1
color: 'IndianRed'
# TODO - moving the color to the common.styl class with definition:
# // The spinner style.
# .qi-spinner
# color: IndianRed
# then removing the color attribute and enabling the className
# attribute below has no effect. Why not?
#className: 'qi-spinner'
)
scope.$watch attrs.qiSpin, (value, oldValue) ->
if value
if not scope.spinner
scope.spinner = createSpinner()
scope.spinner.spin(element[0])
else if scope.spinner
scope.spinner.stop()
# TODO - Refactor the code block into a common service function called
# by the directive.
#
# TODO - Refactor this and the following directive into one directive
# which calls the service function.
#
# TODO - Obtain the range max from the scope.timeSeries.image.volumes.size or
# scope.timeSeries.image.slices.size property. Pass this value as an argument
# to the refactored service function.
#
# TODO - Make a parent directive named qi-axial-select
# (resp. qi-volume-select) that contains the buttons and the slider.
# The parent manages the state and image display. The buttons and slider
# are sibling views. In the current implementation, the slider is the
# de facto parent. State change flows to views, not view to view. Thus,
# the directive is renamed 'qiSliceSelect' (resp. 'qiVolumeSelect').
directives.directive 'qiSliderAxialPlane', ->
(scope) ->
# TODO - The controls should be directives. Should use angular element()
# rather than document.getElementById() to find the elements. Should
# use angular events rather than addEventListener().
# Do this for qiSliderTimeSeries as well.
sliderAxialPlane = document.getElementById('qi-slider-axial-plane')
sliderAxialPlaneBack = document.getElementById('qi-slider-axial-plane-back')
sliderAxialPlaneFwd = document.getElementById('qi-slider-axial-plane-fwd')
# Configure the slider.
#
# TODO - Add tooltip to show current slider value.
#
scope.timeSeries.image.loader.callback = ->
# Obtain the range max from the scope.timeSeries.image.slices.size
# property. This requires that the image is loaded. Therefore, the
# slider compile must be coordinated with the image display compile.
# The slider create must then be deferred as a callback after load
# completes.
rangeMax = scope.timeSeries.image.slices.size
# Create the slider. Pips are placed at each end of the slider (at
# the 0% and 100% positions) and tick marks are added at a density of
# one per slider step.
#
# TODO - Add tooltip to show current slider value.
#
noUiSlider.create sliderAxialPlane,
start: [ scope.slice.sliceNbr ]
step: 1
range:
'min': 1
'max': rangeMax
pips:
mode: 'positions'
values: [0, 100]
density: 100 / (rangeMax - 1)
# When the slider is dragged, get the new slider position and update
# the image.
sliderAxialPlane.noUiSlider.on 'slide', ->
newVal = Math.floor sliderAxialPlane.noUiSlider.get()
updateImage(newVal)
updateImage = (sliceNbr) ->
# Update the image with the new slice number, which corresponds to
# new slider position.
scope.$apply ->
scope.slice.sliceNbr = sliceNbr
# When the back button is clicked, reduce the slider position by one
# and update the image.
sliderAxialPlaneBack.addEventListener 'click', ->
newVal = scope.slice.sliceNbr - 1
# Do the update only if the new value does not fall below 1.
if newVal >= 1
sliderAxialPlane.noUiSlider.set(newVal)
updateImage(newVal)
# When the forward button is clicked, increase the slider position by
# one and update the image.
sliderAxialPlaneFwd.addEventListener 'click', ->
newVal = scope.slice.sliceNbr + 1
# Do the update only if the new value does not go above the number of
# slices in the image axial plane.
if newVal <= scope.timeSeries.image.slices.size
sliderAxialPlane.noUiSlider.set(newVal)
updateImage(newVal)
directives.directive 'qiSliderTimeSeries', ->
(scope) ->
# TODO - The controls should be directives. Should use angular element()
# rather than document.getElementById() to find the elements. Should
# use angular events rather than addEventListener().
sliderTimeSeries = document.getElementById('qi-slider-time-series')
sliderTimeSeriesBack = document.getElementById('qi-slider-time-series-back')
sliderTimeSeriesFwd = document.getElementById('qi-slider-time-series-fwd')
# Check for the existence of the time series slider object to prevent
# the create function from being called twice, which may result from an
# Angular digest anomaly.
if !sliderTimeSeries.noUiSlider
rangeMax = scope.timeSeries.image.volumes.size
# Create the slider. Pips are placed at each end of the slider (at
# the 0% and 100% positions) and tick marks are added at a density of
# one per slider step.
#
# TODO - Add tooltip to show current slider value.
#
noUiSlider.create sliderTimeSeries,
start: [ scope.volume.volumeNbr ]
step: 1
range:
'min': 1
'max': rangeMax
pips:
mode: 'positions'
values: [0, 100]
density: 100 / (rangeMax - 1)
# When the slider is dragged, get the new slider position and update
# the image.
sliderTimeSeries.noUiSlider.on 'slide', ->
newVal = Math.floor sliderTimeSeries.noUiSlider.get()
updateImage(newVal)
updateImage = (volumeNbr) ->
# Update the image with the new volume number, which corresponds to
# new slider position.
scope.$apply ->
scope.volume.volumeNbr = volumeNbr
# When the back button is clicked, reduce the slider position by one
# and update the image.
sliderTimeSeriesBack.addEventListener 'click', ->
newVal = scope.volume.volumeNbr - 1
# Do the update only if the new value does not fall below 1.
if newVal >= 1
sliderTimeSeries.noUiSlider.set(newVal)
updateImage(newVal)
# When the forward button is clicked, increase the slider position by
# one and update the image.
sliderTimeSeriesFwd.addEventListener 'click', ->
newVal = scope.volume.volumeNbr + 1
# Do the update only if the new value does not go above the number of
# volumes in the time series.
if newVal <= scope.timeSeries.image.volumes.size
sliderTimeSeries.noUiSlider.set(newVal)
updateImage(newVal)
directives.directive 'qiFocus', ['$timeout',
($timeout) ->
restrict: 'A'
scope:
focus: '@qiFocus'
link: (scope, element) ->
scope.$watch 'focus', (value) ->
if value is 'true'
$timeout ->
element[0].focus()
]
# Displays the collection charts.
#
# TODO - Why is this not an entity (E) directive?
# Why is the attribute removed after rendering? The intent seems to
# be to disable the directive after the link. However, removing the
# attribute does not appear to have any effect. The directive is
# called on AngularJS compile. Once compiled, the element is not
# recompiled. Thus, removing the attribute is superfluous. Is this
# assessment correct?
# TODO - why is there a config? test. Shouldn't config always exist?
# TODO - Why is $compile called? This call does not appear to have any
# effect. The link is performed during $compile, so calling $compile
# here seems to be at best unnecessary and at worst dangerous.
# TODO - same effect might be obtained more cleanly on each chart
# using the qiCollectionChart directive below.
#
directives.directive 'qiCollectionCharts', ['Collection', '$compile',
(Collection, $compile) ->
restrict: 'A'
link: (scope, element) ->
scope.$watch 'config', (config) ->
if config?
# The page must completely load before the chart rendering call
# is made.
# TODO - element should already be ready before link is called.
# Also, this element ready is no guarantee that the chart
# elements are ready.
element.ready ->
Collection.renderCharts(scope.config)
element.removeAttr('qi-collection-charts')
$compile(element)(scope)
]
# TODO - try enabling this in conjunction with the TODO above.
directives.directive 'qiCollectionChart', ['Collection',
(Collection) ->
restrict: 'E'
link: (scope, element) ->
# Here, the scope config is set on the individual chart, not
# all of the charts.
Collection.renderChart(scope.config)
]
# Displays the slice.
directives.directive 'qiSliceDisplay', ['SliceDisplay', '$compile',
(SliceDisplay, $compile) ->
restrict: 'A'
link: (scope, element) ->
# TODO - obtain the updateImage argument.
# SliceDisplay.updateImage(???)
# TODO - see qiCollectionCharts link TODO.
element.removeAttr('qi-slice-display')
$compile(element)(scope)
]
# Displays a scan or registration series 3D volume.
directives.directive 'qiVolumeDisplay', ->
restrict: 'E'
link: (scope, element) ->
scope.$watch 'volume', (image) ->
if volume.isLoaded()
scope.volume.open(element)