├── server ├── __init__.py ├── models.py ├── templates │ ├── registration │ │ └── login.html │ └── base.html ├── urls.py ├── settings.py └── views.py ├── .dockerignore ├── client ├── templates │ └── study.tmpl ├── css │ ├── button-icons.png │ ├── Aristo │ │ ├── images │ │ │ ├── bg_fallback.png │ │ │ ├── icon_sprite.png │ │ │ ├── progress_bar.gif │ │ │ ├── slider_handles.png │ │ │ ├── ui-icons_222222_256x240.png │ │ │ └── ui-icons_454545_256x240.png │ │ └── jquery-ui-1.8.7.custom.css │ ├── handheld.css │ ├── nanoscroller.css │ ├── reportaconcern.css │ ├── style.css │ └── button-styles.css ├── js │ ├── config.js │ ├── plugins.js │ ├── libs │ │ ├── jquery.mousewheel.js │ │ ├── jquery.nanoscroller.min.js │ │ ├── big.min.js │ │ ├── raphaelle.js │ │ ├── underscore-min.js │ │ └── require.js │ ├── utils.js │ ├── models.js │ ├── reportaconcern.js │ ├── lightbox.js │ ├── views.js │ ├── main.js │ └── ruler.js └── index.html ├── StudyCentric.png ├── wsgi.py ├── env_vars_sample ├── .gitignore ├── Dockerfile ├── scripts ├── http.sh ├── nginx.sh └── write_config.py ├── LICENSE └── README.md /server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | env_vars* 2 | env_var* 3 | -------------------------------------------------------------------------------- /client/templates/study.tmpl: -------------------------------------------------------------------------------- 1 | A Dicom Series 2 | -------------------------------------------------------------------------------- /StudyCentric.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/StudyCentric.png -------------------------------------------------------------------------------- /client/css/button-icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/button-icons.png -------------------------------------------------------------------------------- /client/css/Aristo/images/bg_fallback.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/bg_fallback.png -------------------------------------------------------------------------------- /client/css/Aristo/images/icon_sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/icon_sprite.png -------------------------------------------------------------------------------- /client/css/Aristo/images/progress_bar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/progress_bar.gif -------------------------------------------------------------------------------- /client/css/Aristo/images/slider_handles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/slider_handles.png -------------------------------------------------------------------------------- /client/css/Aristo/images/ui-icons_222222_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/ui-icons_222222_256x240.png -------------------------------------------------------------------------------- /client/css/Aristo/images/ui-icons_454545_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chop-dbhi/studycentric/HEAD/client/css/Aristo/images/ui-icons_454545_256x240.png -------------------------------------------------------------------------------- /wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 3 | 'server.settings') 4 | 5 | from django.core.wsgi import get_wsgi_application 6 | application = get_wsgi_application() 7 | -------------------------------------------------------------------------------- /client/css/handheld.css: -------------------------------------------------------------------------------- 1 | * { 2 | float: none; /* Screens are not big enough to account for floats */ 3 | background: #fff; /* As much contrast as possible */ 4 | color: #000; 5 | } 6 | 7 | /* Slightly reducing font size to reduce need to scroll */ 8 | body { font-size: 80%; } -------------------------------------------------------------------------------- /env_vars_sample: -------------------------------------------------------------------------------- 1 | SECRET_KEY=django_secret_key 2 | LOGIN_ENABLED=Force users to login, 0 or 1 3 | FORCE_SCRIPT_NAME= 4 | DICOM_SERVER= 5 | DICOM_PORT= 6 | 7 | SERVER_WADO_SERVER=If not supplied, defaults to DICOM_SERVER 8 | SERVER_WADO_PORT= 9 | SERVER_WADO_PATH= 10 | DICOM_AET=DCM4CHEE -------------------------------------------------------------------------------- /server/templates/registration/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 | 5 | {% if form.errors %} 6 |

Your username and password didn't match. Please try again.

7 | {% endif %} 8 | 9 |
10 | {% csrf_token %} 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
{{ form.username.label_tag }}{{ form.username }}
{{ form.password.label_tag }}{{ form.password }}
21 | 22 | 23 | 24 |
25 | 26 | {% endblock %} -------------------------------------------------------------------------------- /client/js/config.js: -------------------------------------------------------------------------------- 1 | // Configuration Options for StudyCentric 2 | define({ 3 | StudyCentricProt:"http", 4 | StudyCentricHost:"localhost", 5 | StudyCentricPath:"", 6 | StudyCentricPort:8000, 7 | WADOHost:"localhost", 8 | WADOPort:8080, 9 | WADOProt:"http", 10 | WADOPath:"wado", 11 | InstanceThumbNailSizePx:100, 12 | SeriesThumbNailSizePx:150, 13 | DefaultImgSize:128, 14 | ImagesPerRow:3, 15 | DisableClinicalWarning:false, 16 | MeasurementPrecision:1, 17 | JSONP: false, 18 | HoverColor: "#FFAA56", 19 | HintColor: "#FFFFFF", 20 | MeasurementColor: "#00FF00", 21 | png:false, 22 | EnableReportConcern:false, 23 | ReportConcernUrl:"../concerns/", 24 | ReportConcernEmail:"Enter Email" 25 | }); 26 | -------------------------------------------------------------------------------- /client/js/plugins.js: -------------------------------------------------------------------------------- 1 | 2 | // usage: log('inside coolFunc', this, arguments); 3 | // paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ 4 | window.log = function(){ 5 | log.history = log.history || []; // store logs to an array for reference 6 | log.history.push(arguments); 7 | arguments.callee = arguments.callee.caller; 8 | if(this.console) console.log( Array.prototype.slice.call(arguments) ); 9 | }; 10 | // make it safe to use console.log always 11 | (function(b){function c(){}for(var d="assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","),a;a=d.pop();)b[a]=b[a]||c})(window.console=window.console||{}); 12 | 13 | 14 | // place any jQuery/helper plugins in here, instead of separate, slower script files. 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | *.py[co] 4 | *.sw[po] 5 | dist 6 | sdist 7 | build 8 | pip-log.txt 9 | MANIFEST 10 | .Python 11 | .watch-pid 12 | .sass-cache 13 | testdb 14 | lib/ 15 | bin/ 16 | include/ 17 | gdcm/ 18 | gdcmbin/ 19 | 20 | # ignore local settings file intended for environment-specific settings 21 | local_settings.py 22 | 23 | # ignore optimized javascripts 24 | src/static/scripts/javascript/min 25 | 26 | # all Sass files will be compiled into this directory 27 | src/static/stylesheets/css/*.css 28 | src/static/stylesheets/css/page/*.css 29 | 30 | # Collected static files via Django's staticfiles app 31 | _site/static 32 | 33 | # Do not version user uploaded content since these files might be huge. 34 | # Have another strategy for backing up this content. 35 | _site/media 36 | env_vars_chop 37 | env_var_chop 38 | build.sh 39 | env_vars* 40 | -------------------------------------------------------------------------------- /server/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | {% block content %}{% endblock %} 16 |
17 | 20 | 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # StudyCentric 2 | 3 | FROM ubuntu:vivid 4 | 5 | MAINTAINER Jeff Miller "millerjm1@email.chop.edu" 6 | 7 | RUN apt-get update -qq --fix-missing 8 | RUN apt-get install software-properties-common -y 9 | RUN apt-get install -y\ 10 | build-essential\ 11 | git-core\ 12 | libldap2-dev\ 13 | libpq-dev\ 14 | libsasl2-dev\ 15 | libssl-dev\ 16 | libxml2-dev\ 17 | libxslt1-dev\ 18 | libffi-dev\ 19 | openssl\ 20 | wget\ 21 | zlib1g-dev 22 | 23 | RUN apt-get install -y python2.7 24 | RUN apt-get install -y python2.7-dev 25 | RUN apt-get install -y libgdcm2.4 26 | RUN apt-get install -y python-gdcm 27 | RUN apt-get install -y python-pip 28 | 29 | # Python dependencies 30 | RUN pip install "Django==1.5" 31 | RUN pip install "requests" 32 | RUN pip install "pydicom" 33 | RUN pip install "uWSGI" 34 | 35 | ADD . /opt/app 36 | 37 | # Ensure all python requirements are met 38 | ENV APP_NAME STUDYCENTRIC 39 | 40 | CMD ["/opt/app/scripts/http.sh"] 41 | 42 | EXPOSE 8000 43 | -------------------------------------------------------------------------------- /client/css/nanoscroller.css: -------------------------------------------------------------------------------- 1 | /** initial setup **/ 2 | .nano { 3 | position : relative; 4 | width : 100%; 5 | height : 100%; 6 | overflow : hidden; 7 | } 8 | .nano .content { 9 | position : absolute; 10 | overflow : scroll; 11 | overflow-x : hidden; 12 | top : 0; 13 | right : 0; 14 | bottom : 0; 15 | left : 0; 16 | } 17 | .nano .pane { 18 | background: rgba(0,0,0,.25); 19 | position : absolute; 20 | width : 10px; 21 | right : 0; 22 | top : 0; 23 | bottom : 0; 24 | } 25 | .nano .pane .slider { 26 | background: rgba(0,0,0,.5); 27 | position : relative; 28 | margin : 0 1px 0 1px; 29 | border-radius : 3px; 30 | } 31 | .nano:hover .pane, .pane.active { 32 | visibility: visible; 33 | } 34 | .nano .pane { 35 | -webkit-transition : .2s; 36 | -moz-transition : .2s; 37 | -o-transition : .2s; 38 | transition : .2s; 39 | border-radius : 5px; 40 | -moz-border-radius : 5px; 41 | opacity : .01; 42 | } 43 | .nano:hover .pane, .pane.active { 44 | opacity : 1; 45 | } 46 | -------------------------------------------------------------------------------- /scripts/http.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APP_DIR=/opt/app/ 4 | cd $APP_DIR 5 | 6 | if [ "$LOGIN_ENABLED" = "1" ]; then 7 | PYTHONPATH=$PYTHONPATH:`pwd` DJANGO_SETTINGS_MODULE='server.settings' django-admin.py syncdb --noinput 8 | if [ -n "$DJANGO_ADMIN_USER" ] && [ -n "$DJANGO_ADMIN_EMAIL" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ]; then 9 | echo "Creating admin user" 10 | echo "from django.contrib.auth.models import User; User.objects.create_superuser('"$DJANGO_ADMIN_USER"', '"$DJANGO_ADMIN_EMAIL"', '"$DJANGO_ADMIN_PASSWORD"')" | PYTHONPATH=$PYTHONPATH:`pwd` DJANGO_SETTINGS_MODULE='server.settings' django-admin.py shell 11 | elif [ -n "$DJANGO_ADMIN_USER" ] && [ -n "$DJANGO_ADMIN_EMAIL" ]; then 12 | echo "Creating admin user with no password" 13 | PYTHONPATH=$PYTHONPATH:`pwd` DJANGO_SETTINGS_MODULE='server.settings' django-admin.py createsuperuser --noinput --username=$DJANGO_ADMIN_USER --email=$DJANGO_ADMIN_EMAIL 14 | fi 15 | fi 16 | 17 | 18 | # Write the config file that the JS client requires 19 | python /opt/app/scripts/write_config.py 20 | 21 | exec /usr/local/bin/uwsgi --die-on-term --http-socket 0.0.0.0:8000 -p 4 -b 32768 -T --master --max-requests 5000 --static-map /static=/opt/app/client --module wsgi:application 22 | -------------------------------------------------------------------------------- /server/urls.py: -------------------------------------------------------------------------------- 1 | import re 2 | from views import * 3 | from django.conf.urls import patterns, url 4 | from django.conf import settings 5 | from django.contrib.auth.decorators import login_required 6 | 7 | if not settings.LOGIN_ENABLED: 8 | login_required = lambda x: x 9 | 10 | urlpatterns = patterns('', 11 | url(r'^study/(?P[0-9.]+)/?$', login_required(study)), 12 | url(r'^series/(?P[0-9.]+)/?$', login_required(series)), 13 | url(r'^object/(?P[0-9.]+)/?$', login_required(instance)), 14 | url(r'^wado/?$', login_required(wado)), 15 | ) 16 | 17 | if settings.LOGIN_ENABLED: 18 | urlpatterns += patterns('', 19 | url(r'^app/?$', login_required(app_root)), 20 | # Below enables a simple login that will use the model authorization backend 21 | # Replace with your own auth system if necessary 22 | url(r'^login/?$', 'django.contrib.auth.views.login'), 23 | ) 24 | 25 | 26 | # In production, these two locations must be served up statically 27 | if settings.DEBUG: 28 | urlpatterns += patterns('django.views.static', 29 | url(r'^%s(?P.*)$' % re.escape(settings.STATIC_URL.lstrip('/')), 'serve', { 30 | 'document_root': settings.STATIC_ROOT 31 | }), 32 | ) 33 | -------------------------------------------------------------------------------- /scripts/nginx.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | APP_DIR=/opt/app/ 4 | cd $APP_DIR 5 | 6 | if [ "$LOGIN_ENABLED" = "1" ]; then 7 | PYTHONPATH=$PYTHONPATH:`pwd` DJANGO_SETTINGS_MODULE='server.settings' django-admin.py syncdb --noinput 8 | if [ -n "$DJANGO_ADMIN_USER" ] && [ -n "$DJANGO_ADMIN_EMAIL" ] && [ -n "$DJANGO_ADMIN_PASSWORD" ]; then 9 | echo "Creating admin user" 10 | echo "from django.contrib.auth.models import User; User.objects.create_superuser('"$DJANGO_ADMIN_USER"', '"$DJANGO_ADMIN_EMAIL"', '"$DJANGO_ADMIN_PASSWORD"')" | PYTHONPATH=$PYTHONPATH:`pwd` DJANGO_SETTINGS_MODULE='server.settings' django-admin.py shell 11 | elif [ -n "$DJANGO_ADMIN_USER" ] && [ -n "$DJANGO_ADMIN_EMAIL" ]; then 12 | echo "Creating admin user with no password" 13 | PYTHONPATH=$PYTHONPATH:`pwd` DJANGO_SETTINGS_MODULE='server.settings' django-admin.py createsuperuser --noinput --username=$DJANGO_ADMIN_USER --email=$DJANGO_ADMIN_EMAIL 14 | fi 15 | fi 16 | 17 | # Write the config file that the JS client requires 18 | python /opt/app/scripts/write_config.py 19 | 20 | exec /usr/local/bin/uwsgi --die-on-term --chdir /opt/app --uwsgi-socket 0.0.0.0:8000 -p 4 -b 32768 -T --master --max-requests 5000 --static-map /static=/opt/app/client --module wsgi:application --mount /proband_connect=wsgi.py --manage-script-name 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, The Children's Hospital of Philadelphia 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the 5 | following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following 8 | disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the 11 | following disclaimer in the documentation and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 14 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 15 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 16 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 17 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 18 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 19 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 20 | THIS SOFTWARE IS NOT INTENDED FOR PRIMARY DIAGNOSTIC, ONLY FOR SCIENTIFIC USAGE. 21 | 22 | THIS SOFTWARE IS NOT CERTIFIED AS A MEDICAL DEVICE FOR PRIMARY DIAGNOSIS. THERE ARE NO CERTIFICATIONS. YOU CAN ONLY USE THIS SOFTWARE AS A REVIEWING AND SCIENTIFIC SOFTWARE, NOT FOR PRIMARY DIAGNOSTIC. 23 | 24 | ALL CALCULATIONS, MEASUREMENTS AND IMAGES PROVIDED BY THIS SOFTWARE ARE INTENDED ONLY FOR SCIENTIFIC RESEARCH, NOT FOR DIAGNOSIS. 25 | 26 | THE IMAGES DISPLAYED BY THIS VIEWER WHEN USED IN CONJUNCTION WITH THE DCM4CHEE PACS SERVER ARE LOSSY JPEGS AND ARE NOT OF THE SAME QUALITY AS DISPLAYED BY A MEDICAL DICOM VIEWER. 27 | -------------------------------------------------------------------------------- /scripts/write_config.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | # This script creates a static JavaScript configuration module the StudyCentric client uses 4 | import json 5 | import os 6 | 7 | def get_env_variable(var_name, default=None): 8 | """ Get the environment variable or return an exception""" 9 | try: 10 | return os.environ[var_name] 11 | except KeyError: 12 | return default 13 | 14 | config = { 15 | "StudyCentricPath":get_env_variable("STUDYCENTRIC_PATH", "../"), 16 | "WADOHost":get_env_variable("CLIENT_WADO_HOST") or get_env_variable('WADO_SERVER') or get_env_variable("DICOM_SERVER", "localhost"), 17 | "WADOPort":get_env_variable("CLIENT_WADO_PORT") or get_env_variable('WADO_PORT', 8080), 18 | "WADOProt":get_env_variable("CLIENT_WADO_PROT") or get_env_variable('WADO_PROT', 'http'), 19 | "WADOPath":get_env_variable("CLIENT_WADO_PATH") or get_env_variable('WADO_PATH', 'wado'), 20 | "InstanceThumbNailSizePx":get_env_variable("INSTANCE_THUMBNAIL_SIZE_PX", 100), 21 | "SeriesThumbNailSizePx":get_env_variable("SERIES_THUMBNAIL_SIZE_PX", 150), 22 | "DefaultImgSize":get_env_variable("DEFAULT_IMG_SIZE", 128), 23 | "ImagesPerRow":get_env_variable("IMAGES_PER_ROW", 3), 24 | "DisableClinicalWarning":bool(int(get_env_variable("DISABLE_CLINICAL_WARNING", False))), 25 | "MeasurementPrecision":int(get_env_variable("MEASUREMENT_PRECISION", 1)), 26 | "JSONP": bool(int(get_env_variable("JSONP", True))), 27 | "HoverColor": get_env_variable("HOVER_COLOR","#FFAA56"), 28 | "HintColor": get_env_variable("HINT_COLOR", "#FFFFFF"), 29 | "MeasurementColor": get_env_variable("MEASUREMENT_COLOR", "#00FF00"), 30 | "png": bool(int(get_env_variable("PNG", False))), 31 | "EnableReportConcern": bool(int(get_env_variable("ENABLE_REPORT_CONCERN", False))), 32 | "ReportConcernUrl": get_env_variable("REPORT_CONCERN_URL", "../concerns/"), 33 | "ReportConcernEmail": get_env_variable("REPORT_CONCERN_EMAIL", "Enter Email") 34 | } 35 | 36 | output = "define(%s);" % json.dumps(config) 37 | 38 | f = open("/opt/app/client/js/config.js", "w") 39 | f.write(output) 40 | f.close() 41 | -------------------------------------------------------------------------------- /server/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | def get_env_variable(var_name, default=None): 5 | """ Get the environment variable or return an exception""" 6 | try: 7 | return os.environ[var_name] 8 | except KeyError: 9 | error_msg = "Set the %s environment variable" % var_name 10 | return default 11 | 12 | 13 | SECRET_KEY = get_env_variable("SECRET_KEY", "REPLACE_WITH_REAL_SECRET_KEY") 14 | LOGIN_ENABLED = bool(int(get_env_variable("LOGIN_ENABLED", False))) 15 | 16 | ROOT_URLCONF = 'server.urls' 17 | 18 | DATABASES = { 19 | 'default': { 20 | 'ENGINE': 'django.db.backends.sqlite3', 21 | 'NAME': get_env_variable("DJANGO_DB_NAME", "database.db") 22 | } 23 | } 24 | 25 | DEBUG = bool(get_env_variable('DJANGO_DEBUG', False)) 26 | INSTALLED_APPS = ('server',) 27 | 28 | FORCE_SCRIPT_NAME = get_env_variable('FORCE_SCRIPT_NAME', '') 29 | LOGIN_URL = FORCE_SCRIPT_NAME + '/login/' 30 | LOGIN_REDIRECT_URL = FORCE_SCRIPT_NAME + '/app/' 31 | STATIC_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))+'/client/' 32 | STATIC_URL = "/static/" 33 | 34 | if LOGIN_ENABLED: 35 | INSTALLED_APPS += ('django.contrib.sessions', 36 | 'django.contrib.auth', 37 | 'django.contrib.contenttypes', 38 | 'django.contrib.messages', 39 | 'django.contrib.admin', 40 | 'django.contrib.staticfiles',) 41 | 42 | MIDDLEWARE_CLASSES = ('django.contrib.sessions.middleware.SessionMiddleware', 43 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 44 | 'django.contrib.messages.middleware.MessageMiddleware',) 45 | 46 | TEMPLATE_DIRS = (STATIC_ROOT,) 47 | 48 | SC_DICOM_SERVER = get_env_variable('DICOM_SERVER', 'localhost') 49 | SC_DICOM_PORT = int(get_env_variable('DICOM_PORT', 11112)) 50 | 51 | SC_WADO_SERVER = get_env_variable('WADO_SERVER', False) or SC_DICOM_SERVER 52 | SC_WADO_PORT = int(get_env_variable('WADO_PORT', 8080)) 53 | SC_WADO_PATH = get_env_variable('WADO_PATH', 'wado') 54 | SC_WADO_PROT = get_env_variable('WADO_PROT', 'http') 55 | 56 | AET = get_env_variable('DICOM_AET', 'DCM4CHEE') 57 | 58 | CACHES = { 59 | 'default': { 60 | 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 61 | } 62 | } 63 | try: 64 | from local_settings import * 65 | except ImportError: 66 | pass 67 | -------------------------------------------------------------------------------- /client/js/libs/jquery.mousewheel.js: -------------------------------------------------------------------------------- 1 | /*! Copyright (c) 2011 Brandon Aaron (http://brandonaaron.net) 2 | * Licensed under the MIT License (LICENSE.txt). 3 | * 4 | * Thanks to: http://adomas.org/javascript-mouse-wheel/ for some pointers. 5 | * Thanks to: Mathias Bank(http://www.mathias-bank.de) for a scope bug fix. 6 | * Thanks to: Seamus Leahy for adding deltaX and deltaY 7 | * 8 | * Version: 3.0.6 9 | * 10 | * Requires: 1.2.2+ 11 | */ 12 | define(["jquery"], function(jQuery) { 13 | (function($) { 14 | 15 | var types = ['DOMMouseScroll', 'mousewheel']; 16 | 17 | if ($.event.fixHooks) { 18 | for ( var i=types.length; i; ) { 19 | $.event.fixHooks[ types[--i] ] = $.event.mouseHooks; 20 | } 21 | } 22 | 23 | $.event.special.mousewheel = { 24 | setup: function() { 25 | if ( this.addEventListener ) { 26 | for ( var i=types.length; i; ) { 27 | this.addEventListener( types[--i], handler, false ); 28 | } 29 | } else { 30 | this.onmousewheel = handler; 31 | } 32 | }, 33 | 34 | teardown: function() { 35 | if ( this.removeEventListener ) { 36 | for ( var i=types.length; i; ) { 37 | this.removeEventListener( types[--i], handler, false ); 38 | } 39 | } else { 40 | this.onmousewheel = null; 41 | } 42 | } 43 | }; 44 | 45 | $.fn.extend({ 46 | mousewheel: function(fn) { 47 | return fn ? this.bind("mousewheel", fn) : this.trigger("mousewheel"); 48 | }, 49 | 50 | unmousewheel: function(fn) { 51 | return this.unbind("mousewheel", fn); 52 | } 53 | }); 54 | 55 | 56 | function handler(event) { 57 | var orgEvent = event || window.event, args = [].slice.call( arguments, 1 ), delta = 0, returnValue = true, deltaX = 0, deltaY = 0; 58 | event = $.event.fix(orgEvent); 59 | event.type = "mousewheel"; 60 | 61 | // Old school scrollwheel delta 62 | if ( orgEvent.wheelDelta ) { delta = orgEvent.wheelDelta/120; } 63 | if ( orgEvent.detail ) { delta = -orgEvent.detail/3; } 64 | 65 | // New school multidimensional scroll (touchpads) deltas 66 | deltaY = delta; 67 | 68 | // Gecko 69 | if ( orgEvent.axis !== undefined && orgEvent.axis === orgEvent.HORIZONTAL_AXIS ) { 70 | deltaY = 0; 71 | deltaX = -1*delta; 72 | } 73 | 74 | // Webkit 75 | if ( orgEvent.wheelDeltaY !== undefined ) { deltaY = orgEvent.wheelDeltaY/120; } 76 | if ( orgEvent.wheelDeltaX !== undefined ) { deltaX = -1*orgEvent.wheelDeltaX/120; } 77 | 78 | // Add event and delta to the front of the arguments 79 | args.unshift(event, delta, deltaX, deltaY); 80 | 81 | return ($.event.dispatch || $.event.handle).apply(this, args); 82 | } 83 | 84 | })(jQuery); 85 | }); 86 | -------------------------------------------------------------------------------- /client/js/utils.js: -------------------------------------------------------------------------------- 1 | define(["jquery","underscore", "config"], function($, _, Config){ 2 | var timeout = null; 3 | var req = null; 4 | var t_withPixel = _.template("

Horizontal Spacing: <%=xSpacing%>

Vertical Spacing: <%=ySpacing%>

"); 5 | return { 6 | queryString2Object: function(queryString){ 7 | var regEx = /[?&]([^=]*)=([^&]*)/g; 8 | var match; 9 | var obj = {}; 10 | while(match = regEx.exec(queryString)){ 11 | obj[match[1]]=match[2]; 12 | } 13 | return obj; 14 | }, 15 | processDicomAttributes: function(attrs){ 16 | // if the image displayed does not match this response 17 | // we are done 18 | var lightBox = $("#lightbox"); 19 | if (attrs.objectUID !== this.queryString2Object(lightBox[0].src).objectUID) 20 | return; 21 | // TODO disable zoom if nativeRows is 0 22 | lightBox.data("dicom_attrs", attrs); 23 | if (attrs.xSpacing) { 24 | attrs.xSpacing = parseFloat(attrs.xSpacing); 25 | attrs.ySpacing = parseFloat(attrs.ySpacing); 26 | $("#pixel_spacing").html(t_withPixel(attrs)); 27 | } 28 | else $("#pixel_spacing").text("Measurements in pixels"); 29 | // If we don't have windowWidth params, default to sensible 30 | if (!attrs.windowWidth || !attrs.windowCenter) attrs.defaults = true; 31 | attrs.windowWidth = attrs.windowWidth || 50; 32 | attrs.windowCenter = attrs.windowCenter || 200; 33 | //TODO set message for native resolution here, and specify whether it existed or not. 34 | attrs.nativeCols = attrs.nativeCols || 512; 35 | attrs.nativeRows = attrs.nativeRows || 512; 36 | $("body").trigger("dicom_attr_received"); 37 | if (attrs.pixelMessage) $("#pixel_message").text(attrs.pixelMessage); 38 | }, 39 | retrieveDicomAttributes: function(study,imgSource){ 40 | if (timeout !== null){ 41 | clearTimeout(timeout); 42 | } 43 | $("body").trigger("null_dicom_attr"); 44 | if (req !== null) req.abort(); 45 | var that = this; 46 | timeout = setTimeout(function() { 47 | var o = that.queryString2Object(imgSource); 48 | var url = (Config.StudyCentricHost ? ((Config.StudyCentricProt || "https") + "://" + Config.StudyCentricHost + (Config.StudyCentricPort ? ":" + Config.StudyCentricPort:"") +"/") : "" ) + Config.StudyCentricPath +"object/"+o.objectUID+"?&seriesUID="+o.seriesUID+"&studyUID="+o.studyUID; 49 | if (Config.JSONP) 50 | url += "&callback=?"; 51 | req = $.getJSON(url, $.proxy(that.processDicomAttributes, that) ); 52 | }, 500); 53 | } 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /client/js/models.js: -------------------------------------------------------------------------------- 1 | define(['jquery', 2 | 'config', 3 | 'backbone', 4 | 'views'], function($, Config, Backbone, Views) { 5 | 6 | var Instance = Backbone.Model.extend({ 7 | initialize:function(){ 8 | var view = new Views.InstanceView({model:this}); 9 | // TODO this awful, its here to enable putting the red box around instances during scroll 10 | this.bind("scroll_show", function(){$("img", view.el).trigger("click");}); 11 | } 12 | }); 13 | 14 | var ImageStack = Backbone.Collection.extend({ 15 | model:Instance, 16 | parse: function(response){ 17 | var models = []; 18 | for (var index in response){ 19 | models.push({uid:response[index], studyUID:this.studyUID, seriesUID:this.seriesUID}); 20 | } 21 | return models; 22 | }, 23 | initialize: function(){ 24 | //var view = new Views.InstanceStackView({collection:this}); 25 | } 26 | }); 27 | 28 | var Series = Backbone.Model.extend({ 29 | initialize: function(){ 30 | var ref = this; 31 | var data = "json"; 32 | this.instances = new ImageStack(); 33 | // Store study and instance uid on collection so we can properly populate instance model 34 | this.instances.studyUID = this.get("studyUID"); 35 | this.instances.seriesUID=this.get("uid"); 36 | this.instances.url = (Config.StudyCentricHost ? ((Config.StudyCentricProt || "https") + "://" + Config.StudyCentricHost + (Config.StudyCentricPort ? ":"+Config.StudyCentricPort : "") + "/") : "" ) + Config.StudyCentricPath + "series/"+this.get("uid")+"?callback=?"; 37 | var view = new Views.SeriesView({model:this}); 38 | if (Config.JSONP) 39 | data = "jsonp"; 40 | this.instances.fetch({dataType:data}); 41 | } 42 | }); 43 | 44 | var SeriesStack = Backbone.Collection.extend({ 45 | model:Series, 46 | parse: function(response){ 47 | var models = []; 48 | var series_objs=response["series"]; 49 | for (var index in series_objs){ 50 | models.push({uid:series_objs[index]["uid"], description:series_objs[index]["description"], studyUID:this.studyUID}); 51 | } 52 | // TODO remove this 53 | $("#study_description").text(response.description); 54 | return models; 55 | } 56 | }); 57 | 58 | var Study = Backbone.Model.extend({ 59 | initialize: function(){ 60 | var data = "json"; 61 | this.series = new SeriesStack(); 62 | // Stores study uid on collection so we can populate series model 63 | this.series.studyUID = this.get("uid"); 64 | this.series.url = (Config.StudyCentricHost ? ((Config.StudyCentricProt || "https")+"://"+Config.StudyCentricHost + (Config.StudyCentricPort ? ":"+Config.StudyCentricPort : "") + "/") : "") + Config.StudyCentricPath + "study/" + this.get("uid")+"?callback=?"; 65 | if (Config.JSONP) 66 | data = "jsonp"; 67 | this.series.fetch({dataType:data}); 68 | } 69 | }); 70 | 71 | return { 72 | ImageStack:ImageStack, 73 | SeriesStack:SeriesStack, 74 | Series:Series, 75 | Study:Study, 76 | Instance:Instance 77 | }; 78 | }); 79 | -------------------------------------------------------------------------------- /client/css/reportaconcern.css: -------------------------------------------------------------------------------- 1 | .ui-dialog { 2 | -moz-box-shadow: 0 5px 20px 0 #666666; 3 | -webkit-box-shadow: 0 5px 20px 0 #666666; 4 | box-shadow: 0 5px 20px 0 #666666; 5 | position: absolute; } 6 | 7 | .ui-dialog > .container { 8 | -moz-box-shadow: none; 9 | -webkit-box-shadow: none; 10 | box-shadow: none; } 11 | 12 | .ui-dialog.ui-corner-all { 13 | -moz-border-radius: 3px 3px 0 0; 14 | border-radius: 3px 3px 0 0; } 15 | 16 | .ui-dialog.ui-widget-content, .ui-dialog > .ui-widget-content { 17 | border: none; 18 | background-image: none; 19 | color: inherit; } 20 | 21 | .ui-dialog.ui-widget { 22 | font: inherit; } 23 | 24 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header { 25 | -moz-border-radius: 4px 4px 0 0; 26 | border-radius: 4px 4px 0 0; 27 | color: #eeeeee; 28 | text-shadow: 0 1px 2px #3b3b3b; 29 | background-color: #444; 30 | -moz-box-shadow: inset 0 1px 2px #919191, inset 0 -1px 2px #2b2b2b; 31 | -webkit-box-shadow: inset 0 1px 2px #919191, inset 0 -1px 2px #2b2b2b; 32 | box-shadow: inset 0 1px 2px #919191, inset 0 -1px 2px #2b2b2b; 33 | border: 1px solid #111111; 34 | padding: 4px 10px; 35 | font-weight: 500; 36 | font-size: 1.1em; 37 | background-image: none; } 38 | 39 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header::after { 40 | border-bottom: 1px solid #fff; } 41 | 42 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header .ui-dialog-title { 43 | text-shadow: 0 1px 2px #3B3B3B; 44 | margin: 0; } 45 | 46 | .ui-dialog > .ui-dialog-titlebar.ui-widget-header .ui-dialog-titlebar-close { 47 | display: none; } 48 | 49 | .ui-dialog .ui-dialog-content, .ui-dialog .ui-dialog-content > .content { 50 | background: #eee; } 51 | 52 | .ui-dialog .container.ui-dialog-content { 53 | padding-top: 0; 54 | } 55 | 56 | .ui-dialog .ui-dialog-buttonpane { 57 | font-size: 0.8em; 58 | margin: 0 !important; 59 | padding: 0 10px 10px 10px !important; 60 | text-align: right; 61 | background-color: #eee; 62 | border: none !important; } 63 | 64 | .ui-dialog .ui-dialog-buttonpane button, .ui-dialog .ui-dialog-buttonpane button { 65 | margin-left: 10px; } 66 | 67 | 68 | #rac { 69 | -moz-border-radius: 3px; 70 | border-radius: 3px; 71 | background-color: #dddddd; 72 | border: 1px solid #aaaaaa; 73 | -moz-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #c4c4c4, 0 1px 2px #aaaaaa; 74 | -webkit-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #c4c4c4, 0 1px 2px #aaaaaa; 75 | box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #c4c4c4, 0 1px 2px #aaaaaa; 76 | color: #222222; 77 | text-shadow: 0 1px 2px #d4d4d4; 78 | cursor: pointer; 79 | padding: 2px 5px; 80 | font-weight: 600; 81 | font-size: 0.9em; 82 | border: 1px solid #666; 83 | background-color: #ff9999; 84 | border: 1px solid red; 85 | -moz-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #ff6666, 0 1px 2px #d65c5c; 86 | -webkit-box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #ff6666, 0 1px 2px #d65c5c; 87 | box-shadow: inset 0 1px 2px white, inset 0 -1px 2px #ff6666, 0 1px 2px #d65c5c; 88 | padding: 1px 5px; 89 | color: #330000; 90 | text-decoration:none; 91 | } 92 | 93 | #rac.success { 94 | background-color: #00cc00; 95 | border: 1px solid #003300; 96 | -moz-box-shadow: inset 0 1px 2px #85e085, inset 0 -1px 2px #009900, 0 1px 2px #145214; 97 | -webkit-box-shadow: inset 0 1px 2px #85e085, inset 0 -1px 2px #009900, 0 1px 2px #145214; 98 | box-shadow: inset 0 1px 2px #85e085, inset 0 -1px 2px #009900, 0 1px 2px #145214; 99 | color: black; } 100 | 101 | #rac-dialog { 102 | display: none; } 103 | 104 | #rac-dialog textarea { 105 | height: 100px; 106 | width: 368px; 107 | font-size: 0.85em; 108 | } 109 | 110 | #rac:active{ 111 | top:17px; 112 | } 113 | -------------------------------------------------------------------------------- /client/js/libs/jquery.nanoscroller.min.js: -------------------------------------------------------------------------------- 1 | define(["jquery"],function(jQuery){(function(){var c,e,d=function(b,a){return function(){return b.apply(a,arguments)}};c=this.jQuery;e=function(){function b(a){this.el=a;this.generate();this.createEvents();this.addEvents();this.reset()}b.prototype.createEvents=function(){return this.events={down:d(function(a){this.isDrag=!0;this.offsetY=a.clientY-this.slider.offset().top;this.pane.addClass("active");c(document).bind("mousemove",this.events.drag);c(document).bind("mouseup",this.events.up);return!1},this),drag:d(function(a){this.sliderY= 2 | a.clientY-this.el.offset().top-this.offsetY;this.scroll();return!1},this),up:d(function(){this.isDrag=!1;this.pane.removeClass("active");c(document).unbind("mousemove",this.events.drag);c(document).unbind("mouseup",this.events.up);return!1},this),resize:d(function(){return this.reset()},this),panedown:d(function(a){this.sliderY=a.clientY-this.el.offset().top-0.5*this.sliderH;this.scroll();return this.events.down(a)},this),scroll:d(function(){if(!0!==this.isDrag)return this.slider.css({top:this.content[0].scrollTop/ 3 | (this.content[0].scrollHeight-this.content[0].clientHeight)*(this.paneH-this.sliderH)+"px"})},this),wheel:d(function(a){this.sliderY+=-a.wheelDeltaY||-a.delta;this.scroll();return!1},this)}};b.prototype.addEvents=function(){c(window).bind("resize",this.events.resize);this.slider.bind("mousedown",this.events.down);this.pane.bind("mousedown",this.events.panedown);this.content.bind("scroll",this.events.scroll);if(window.addEventListener)return this.pane[0].addEventListener("mousewheel",this.events.wheel), 4 | this.pane[0].addEventListener("DOMMouseScroll",this.events.wheel)};b.prototype.removeEvents=function(){c(window).unbind("resize",this.events.resize);this.slider.unbind("mousedown",this.events.down);this.pane.unbind("mousedown",this.events.panedown);this.content.unbind("scroll",this.events.scroll);if(window.addEventListener)return this.pane[0].removeEventListener("mousewheel",this.events.wheel),this.pane[0].removeEventListener("DOMMouseScroll",this.events.wheel)};b.prototype.getScrollbarWidth=function(){var a, 5 | b,c;b=document.createElement("div");b.style.position="absolute";b.style.width="100px";b.style.height="100px";b.style.overflow="scroll";document.body.appendChild(b);a=b.offsetWidth;c=b.scrollWidth;document.body.removeChild(b);return a-c};b.prototype.generate=function(){this.el.append('
');this.content=c(this.el.children()[0]);this.slider=this.el.find(".slider");this.pane=this.el.find(".pane");this.scrollW=this.getScrollbarWidth();if(0===this.scrollbarWidth)this.scrollW= 6 | 0;this.content.css({right:-this.scrollW+"px"});null!=c.browser.msie&&8>parseInt(c.browser.version)&&this.pane.hide()};b.prototype.reset=function(){if(!0===this.isDead)this.isDead=!1,this.pane.show(),this.addEvents();this.contentH=this.content[0].scrollHeight+this.scrollW;this.paneH=this.pane.outerHeight();this.sliderH=this.paneH/this.contentH*this.paneH;this.sliderH=Math.round(this.sliderH);this.scrollH=this.paneH-this.sliderH;this.slider.height(this.sliderH);this.paneH>=this.content[0].scrollHeight? 7 | this.pane.hide():this.pane.show()};b.prototype.scroll=function(){var a;this.sliderY=Math.max(0,this.sliderY);this.sliderY=Math.min(this.scrollH,this.sliderY);a=this.paneH-this.contentH+this.scrollW;a=a*this.sliderY/this.scrollH;this.content.scrollTop(-a);return this.slider.css({top:this.sliderY})};b.prototype.scrollBottom=function(a){this.reset();this.content.scrollTop(this.contentH-this.content.height()-a)};b.prototype.scrollTop=function(a){this.reset();this.content.scrollTop(a+0)};b.prototype.stop= 8 | function(){this.isDead=!0;this.removeEvents();this.pane.hide()};return b}();c.fn.nanoScroller=function(b){var a;b||(b={});a=this.data("scrollbar");if(void 0===a)a=new e(this,b),this.data({scrollbar:a});else return b.scrollBottom?a.scrollBottom(b.scrollBottom):b.scrollTop?a.scrollTop(b.scrollTop):"bottom"===b.scroll?a.scrollBottom(0):"top"===b.scroll?a.scrollTop(0):!0===b.stop?a.stop():a.reset()}}).call(this);}); 9 | -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | StudyCentric 14 | 15 | 16 | 17 | 18 | 19 | 34 | 44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 | 57 |
58 |
59 |
60 |
61 | 62 | 68 | 69 | 72 | 82 | 83 | 84 | 89 | 92 | 93 | 94 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /client/js/reportaconcern.js: -------------------------------------------------------------------------------- 1 | define(["jquery","underscore","utils","config","libs/jquery-ui-min"], function($, _, Utils, Config) { 2 | if (!Config.EnableReportConcern){ 3 | return; 4 | } 5 | 6 | function loadCss(url) { 7 | var link = document.createElement("link"); 8 | link.type = "text/css"; 9 | link.rel = "stylesheet"; 10 | link.href = url; 11 | document.getElementsByTagName("head")[0].appendChild(link); 12 | } 13 | loadCss("css/reportaconcern.css"); 14 | var phiReportTmpl = _.template(['', 15 | '', 16 | '', 17 | '', 18 | 'Reported PHI Issue', 19 | '', 20 | '', 21 | '
', 22 | 'Study', 23 | '', 24 | ''].join('')); 25 | 26 | var racTmpl =['
', 27 | '
', 28 | '

When you click "Report Concern", information about the ', 29 | 'currently displayed study will be sent to the administrators. Please ', 30 | 'provide any additional information below.

', 31 | '', 32 | '
', 33 | '
'].join(''); 34 | 35 | var errorFallBackTmpl = _.template(['', 36 | 'The submission failed, but we take privacy issues very seriously. ', 37 | 'Please send an email to '+Config.ReportConcernEmail+' ', 38 | 'with the following study id:
<%=study%>
and as much detail about your concern ', 39 | 'as you can WITHOUT revealing any private data in the email itself. Thank you for your help.', 40 | '
'].join('')); 41 | // Setup the privacy concern button 42 | var rac = $('Report Privacy Concern'); 43 | rac.css('position', 'absolute'); 44 | rac.css('left', '250px'); 45 | rac.css('top', '20px'); 46 | $("#header").append(rac); 47 | var racDialog = $(racTmpl); 48 | var save_handler; 49 | rac.click(function(evt){ 50 | evt.preventDefault(); 51 | racDialog.dialog('open'); 52 | }); 53 | var racText = rac.text(); 54 | var comment = racDialog.find('textarea'); 55 | racDialog.dialog({ 56 | autoOpen: false, 57 | resizable: false, 58 | title: 'Report a PHI Concern', 59 | width: 400, 60 | buttons: { 61 | 'Cancel': function() { 62 | racDialog.dialog('close'); 63 | }, 64 | 'Report Concern': function() { 65 | $.ajax({ 66 | type: 'POST', 67 | url: rac.attr('href'), 68 | beforeSend: function(xhr, settings) { 69 | racDialog.dialog('close'); 70 | var context = { 71 | study_link : location.href, 72 | image_link : $("#lightbox").attr("src") 73 | }; 74 | var payload = phiReportTmpl(context); 75 | settings.data = $.param({ 76 | document: payload, 77 | comment: comment.val() 78 | }); 79 | }, 80 | success: function() { 81 | comment.val(''); 82 | rac.addClass('success').text('Submitted. Thank You!'); 83 | setTimeout(function() { 84 | rac.removeClass('success').text(racText); 85 | }, 3000); 86 | }, 87 | error: function(xhr, code, error) { 88 | var failMessage = $(errorFallBackTmpl({study: Utils.queryString2Object(location.href).studyUID})); 89 | racDialog.dialog('open'); 90 | comment.replaceWith(failMessage); 91 | }, 92 | timeout:7000 93 | }); 94 | } 95 | } 96 | }); 97 | }); 98 | -------------------------------------------------------------------------------- /client/js/libs/big.min.js: -------------------------------------------------------------------------------- 1 | /* big.js v2.0.0 https://github.com/MikeMcl/big.js/LICENCE */(function(n){"use strict";function t(n){var u,i,f,r=this;if(!(r instanceof t))return new t(n);if(n instanceof t){r.s=n.s,r.e=n.e,r.c=n.c.slice();return}if(n===0&&1/n<0)n="-0";else if(!l.test(n+=""))throw NaN;for(r.s=n.charAt(0)=="-"?(n=n.slice(1),-1):1,(u=n.indexOf("."))>-1&&(n=n.replace(".","")),(i=n.search(/e/i))>0?(u<0&&(u=i),u+=+n.slice(i+1),n=n.substring(0,i)):u<0&&(u=n.length),i=0;n.charAt(i)=="0";i++);if(i==(f=n.length))r.c=[r.e=0];else{for(;n.charAt(--f)=="0";);for(r.e=u-i-1,r.c=[],u=0;i<=f;r.c[u++]=+n.charAt(i++));}}function o(n,t,i,r){var u=n.c,f=n.e+t+1;if(i!==0&&i!==1&&i!==2)throw"!Big.RM!";if(i=i&&(u[f]>5||u[f]==5&&(i==1||r||f<0||u[f+1]!=null||u[f-1]&1)),f<1||!u[0])n.c=i?(n.e=-t,[1]):[n.e=0];else{if(u.length=f--,i)for(;++u[f]>9;)u[f]=0,f--||(++n.e,u.unshift(1));for(f=u.length;!u[--f];u.pop());}return n}function r(n,i){var s,e=n.c,o=(i=new t(i)).c,r=n.s,h=i.s,u=n.e,f=i.e;if(!e[0]||!o[0])return e[0]?r:o[0]?-h:0;if(r!=h)return r;if(s=r<0,u!=f)return u>f^s?1:-1;for(r=-1,h=(u=e.length)<(f=o.length)?u:f;++ro[r]^s?1:-1;return u==f?0:u>f^s?1:-1}function h(n,i,r){var u=i-(n=new t(n)).e,e=n.c;for(e.length>++i&&o(n,u,t.RM),u=e[0]?r?i:(e=n.c,n.e+u+1):u+1;e.length1?(e.splice(1,0,"."),e.join("")):e[0])+(u<0?"e":"e+")+u:n.toString()}t.DP=20,t.RM=1;var u=1e6,c=1e6,f=-7,e=21,i=t.prototype,l=/^-?\d+(?:\.\d+)?(?:e[+-]?\d+)?$/i,s=new t(1);i.abs=function(){return new t(this)},i.div=function(n){var w=this,c=w.c,e=(n=new t(n)).c,y=w.s==n.s?1:-1,l=t.DP;if(l!==~~l||l<0||l>u)throw"!Big.DP!";if(!c[0]||!e[0]){if(c[0]==e[0])throw NaN;if(!e[0])throw y/0;return new t(y*0)}var h,b,p,a,f,tt=e.slice(),k=h=e.length,it=c.length,i=c.slice(0,h),r=i.length,v=new t(s),d=v.c=[],g=0,nt=l+(v.e=w.e-n.e)+1;for(v.s=y,y=nt<0?0:nt,tt.unshift(0);r++r?1:-1;else for(f=-1,a=0;++fi[f]?1:-1;break}if(a<0){for(b=r==h?e:tt;r;){if(i[--r]nt&&o(v,l,t.RM,i[0]!=null),v},i.eq=function(n){return!r(this,n)},i.gt=function(n){return r(this,n)>0},i.gte=function(n){return r(this,n)>-1},i.lt=function(n){return r(this,n)<0},i.lte=function(n){return r(this,n)<1},i.minus=function(n){var e,o,s,l,h=this,f=h.s,r=(n=new t(n)).s;if(f!=r)return n.s=-r,h.plus(n);var i=h.c,a=h.e,u=n.c,c=n.e;if(!i[0]||!u[0])return u[0]?(n.s=-r,n):new t(i[0]?h:0);if(i=i.slice(),f=a-c){for(e=(l=f<0)?(f=-f,i):(c=a,u),e.reverse(),r=f;r--;e.push(0));e.reverse()}else for(s=((l=i.length0)for(;r--;i[s++]=0);for(r=u.length;r>f;){if(i[--r]0?(s=h,u):(r=-r,i),e.reverse();r--;e.push(0));e.reverse()}for(i.length-u.length<0&&(e=u,u=i,i=e),r=u.length,f=0;r;f=(i[--r]=i[r]+u[r]+f)/10^0,i[r]%=10);for(f&&(i.unshift(f),++s),r=i.length;i[--r]==0;i.pop());return n.c=i,n.e=s,n},i.pow=function(n){var u=n<0,i=new t(this),r=s;if(n!==~~n||n<-c||n>c)throw"!pow!";for(n=u?-n:n;;){if(n&1&&(r=r.times(i)),n>>=1,!n)break;i=i.times(i)}return u?s.div(r):r},i.round=function(n,i){var r=new t(this);if(n==null)n=0;else if(n!==~~n||n<0||n>u)throw"!round!";return o(r,n,i==null?t.RM:i),r},i.sqrt=function(){var u,n,f,r=this,s=r.c,i=r.s,e=r.e,h=new t("0.5");if(!s[0])return new t(r);if(i<0)throw NaN;i=Math.sqrt(r.toString()),i==0||i==1/0?(u=s.join(""),u.length+e&1||(u+="0"),n=new t(Math.sqrt(u).toString()),n.e=((e+1)/2|0)-(e<0||e&1)):n=new t(i.toString()),i=n.e+(t.DP+=4);do f=n,n=h.times(f.plus(r.div(f)));while(f.c.slice(0,i).join("")!==n.c.slice(0,i).join(""));return o(n,t.DP-=4,t.RM),n},i.times=function(n){var i,h=this,e=h.c,o=(n=new t(n)).c,s=e.length,r=o.length,f=h.e,u=n.e;if(n.s=h.s==n.s?1:-1,!e[0]||!o[0])return new t(n.s*0);for(n.e=f+u,s-1;f--){for(r=0,u=s+f;u>f;r=i[u]+o[f]*e[u-f-1]+r,i[u--]=r%10|0,r=r/10|0);r&&(i[u]=(i[u]+r)%10)}for(r&&++n.e,i[0]||i.shift(),u=i.length;!i[--u];i.pop());return n.c=i,n},i.toString=i.valueOf=function(){var r=this,t=r.e,n=r.c.join(""),i=n.length;if(t<=f||t>=e)n=n.charAt(0)+(i>1?"."+n.slice(1):"")+(t<0?"e":"e+")+t;else if(t<0){for(;++t;n="0"+n);n="0."+n}else if(t>0)if(++t>i)for(t-=i;t--;n+="0");else t1&&(n=n.charAt(0)+"."+n.slice(1));return r.s<0&&r.c[0]?"-"+n:n},i.toExponential=function(n){if(n==null)n=this.c.length-1;else if(n!==~~n||n<0||n>u)throw"!toExp!";return h(this,n,1)},i.toFixed=function(n){var t,i=this,r=f,o=e;if(f=-(e=1/0),n==null?t=i.toString():n===~~n&&n>=0&&n<=u&&(t=h(i,i.e+n),i.s<0&&i.c[0]&&t.indexOf("-")<0&&(t="-"+t)),f=r,e=o,!t)throw"!toFix!";return t},i.toPrecision=function(n){if(n==null)return this.toString();if(n!==~~n||n<1||n>u)throw"!toPre!";return h(this,n-1,2)},typeof module!="undefined"&&module.exports?module.exports=t:typeof define=="function"&&define.amd?define(function(){return t}):n.Big=t})(this) -------------------------------------------------------------------------------- /client/js/lightbox.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "ruler","libs/big.min", "libs/raphaelle"], function($, ruler, Big) { 2 | // The lightbox object will represent the image at the center of the screen when measuring is enabled 3 | var LightBox = function(id){ 4 | var that = {}; 5 | var dcmImageLink = $("#"+id); 6 | var dcm = dcmImageLink.get(0); 7 | var width = dcm.width; 8 | var height = dcm.height; 9 | var attrs = dcmImageLink.data("dicom_attrs"); 10 | var ratio = Big(height).div(attrs.nativeRows); 11 | var imgTop = dcmImageLink.css("top"); 12 | var imgLeft = dcmImageLink.css("left"); 13 | var lightBoxDiv = $(''); 14 | var offset = null; // when this was stage.node, moving messed up lines 15 | var top = null; 16 | var left = null; 17 | var ruler_cache = []; 18 | 19 | var container = dcmImageLink.parent(); 20 | 21 | // Make sure the Raphael Box is in the same spot on the screen as the img tag was. 22 | // Needs to be relative because it will not retain its position when moved either wise. 23 | lightBoxDiv.css("top",imgTop).css("left",imgLeft).css("position","absolute"); 24 | 25 | lightBoxDiv.height(height); 26 | lightBoxDiv.width(width); 27 | dcmImageLink.detach(); 28 | $(container).append(lightBoxDiv); 29 | 30 | var paper = Raphael("lightbox",width,height); 31 | 32 | // IE6 and 7 have odd behavior, the box shifts over the right for some reason. 33 | // This hack fixes that. Webkit also has an very odd bug where it shifts over to the 34 | // left even thought the wrapped jquery object should be empty and this should do nothing. 35 | if ($("#lightBox > div").length > 0) { 36 | $("#lightBox > div").css("position","relative"); 37 | } 38 | 39 | that.paper = paper; 40 | that.width = width; 41 | 42 | var stage = paper.image(dcmImageLink.attr('src'),0,0,width,height); 43 | 44 | var node = $(stage.node).parent(); // Passing the Raphael Object was not working because it didn't have a node property 45 | 46 | var moveCursorOn = function(cursor){ 47 | lightBoxDiv.toggleClass("crosshair",false); 48 | lightBoxDiv.toggleClass("move",true); 49 | 50 | }; 51 | that.moveCursorOn = moveCursorOn; 52 | 53 | var moveCursorOff = function(cursor){ 54 | lightBoxDiv.toggleClass("move",false); 55 | lightBoxDiv.toggleClass("crosshair",true); 56 | }; 57 | that.moveCursorOff = moveCursorOff; 58 | 59 | var measureOn = function() { 60 | 61 | // Show the user a crosshair so they know they can measure 62 | lightBoxDiv.hover(function(){ lightBoxDiv.toggleClass("crosshair",true);}, function(){lightBoxDiv.toggleClass("crosshair",false);}); 63 | var currentRuler = null; 64 | 65 | var mouseDown = function (event, movedRuler, seedX, seedY) { 66 | var x = null; 67 | var y = null; 68 | // We need to re-calculate this because if the user changes the size of the browser, it can change. 69 | offset = $(lightBoxDiv).offset(); // when this was stage.node, moving messed up lines 70 | 71 | top = offset.top; 72 | left = offset.left; 73 | 74 | if (event == null){ 75 | x = seedX; 76 | y = seedY; 77 | currentRuler = movedRuler; 78 | }else{ 79 | x = event.pageX - Math.floor(left); // Firefox has a bug here, giving non-integer back. 80 | y = event.pageY - top; 81 | currentRuler = ruler(that, ratio, attrs.xSpacing, attrs.ySpacing); 82 | } 83 | currentRuler.start({x:x,y:y}); 84 | 85 | $(node).unbind("mousedown"); 86 | 87 | $(document).bind("mousemove.measure", function (event) { 88 | var endX = event.pageX - Math.floor(left); 89 | var endY = event.pageY - top; 90 | currentRuler.adjust({x:endX, y:endY}); 91 | return false; 92 | }); 93 | 94 | var measureDone = function (event){ 95 | // For ie, we can't bind to the node object for some reason, so we need to prevent a line from drawing 96 | // if the user is not inside the image. 97 | if ((event.pageY > top + height) || (event.pageY < top) || (event.pageX > left + width) || (event.pageX < left)){ 98 | return; 99 | } 100 | 101 | var endX = event.pageX - Math.floor(left); 102 | var endY = event.pageY - top; 103 | 104 | $(document).unbind(".measure"); 105 | 106 | $(node).mousedown(mouseDown); 107 | currentRuler.end({x:endX,y:endY}); 108 | ruler_cache.push(currentRuler); 109 | currentRuler = null; 110 | }; 111 | 112 | $(document).bind("mouseup.measure",measureDone); 113 | $(document).bind("mousedown.measure",measureDone); 114 | 115 | // Prevent a small square from appearing when clicking on SVGs 116 | // and text cursor from appearing on drag. 117 | if (event !== null) { 118 | event.preventDefault(); 119 | event.stopPropagation(); 120 | } 121 | }; 122 | that.mouseDown = mouseDown; 123 | $(node).mousedown(mouseDown); 124 | }; 125 | that.measureOn = measureOn; 126 | 127 | var measureOff = function(){ 128 | $(document).unbind(".measure"); 129 | $(node).unbind("mousedown"); 130 | }; 131 | that.measureOff = measureOff; 132 | 133 | // revert back to plain old image tag 134 | var destroy = function() { 135 | lightBoxDiv.remove(); 136 | $(container).append(dcmImageLink); 137 | }; 138 | that.destroy = destroy; 139 | 140 | var clear = function(){ 141 | $.each(ruler_cache, function(){ 142 | this.clear(); 143 | }); 144 | ruler_cache = []; 145 | }; 146 | that.clear = clear; 147 | return that; 148 | }; 149 | return LightBox; 150 | }); 151 | -------------------------------------------------------------------------------- /client/js/views.js: -------------------------------------------------------------------------------- 1 | define(["jquery", 2 | "config", 3 | "backbone", 4 | "underscore", 5 | "utils"], function($,Config,Backbone,_, Utils){ 6 | 7 | var SeriesView = Backbone.View.extend({ 8 | tagName: "div", 9 | className: "series", 10 | events: { 11 | "click":"openSeries" 12 | }, 13 | initialize: function() { 14 | _.bindAll(this, "render"); 15 | $("#series .content").append(this.el); 16 | this.template = _.template($("#seriesview").html()); 17 | this.model.instances.bind("reset",this.render); 18 | }, 19 | 20 | render: function() { 21 | $(this.el).html(this.template({ 22 | wado_url: (Config.WADOHost === '/' ? '/' : (Config.WADOHost ? ((Config.WADOProt || "https") + "://" + Config.WADOHost+(Config.WADOPort ? ":"+Config.WADOPort : "") +"/"): "" )) 23 | + Config.WADOPath + "?" + (Config.png ? "contentType=image%2Fpng&":"contentType=image%2Fjpeg&") + "requestType=WADO&studyUID="+this.model.get("studyUID")+"&" 24 | + "seriesUID="+this.model.get("uid") + "&" + "objectUID=" + this.model.instances.at(0).get("uid") 25 | + "&rows=100", 26 | size:Config.SeriesThumbNailSizePx, 27 | description:this.model.get("description") || "None" 28 | })); 29 | var save_el; 30 | var save_parent; 31 | var label; 32 | var interval 33 | $("img", this.el).hover(function(){ 34 | if ($("#lightbox").hasClass("ui-draggable-dragging")) return; 35 | save_parent = $(this).parent(); 36 | var offset = 0; 37 | save_el = $(this).siblings(".full_series_description"); 38 | if (save_el.width() - 10 > $(this).width()) offset = 15; 39 | label = $(this).siblings(".series_description").css("visibility", "hidden"); 40 | save_el.appendTo("body").css("position","absolute").css("top", label.offset().top).css("left", label.offset().left-offset).show(); 41 | interval = setInterval(function(){ 42 | save_el && save_el.css("top", save_parent.offset().top); 43 | }, 50); 44 | }, 45 | function(){ 46 | if ($("#lightbox").hasClass("ui-draggable-dragging")) return; 47 | clearInterval(interval); 48 | save_el.hide().appendTo(save_parent); 49 | label && label.css("visibility", "visible"); 50 | }); 51 | }, 52 | openSeries:function(){ 53 | $("#slider").slider("disable"); 54 | // We already have all the UIDs for this study 55 | // Clear out the models section, and put the new models in there 56 | // Also reset tool to the move tool and clear out the stage 57 | var instance_view = new InstanceStack({collection:this.model.instances}); 58 | // TODO we maintain state about the current series wwwl in the src, this is not good 59 | // we have to clean it out so the next study doesn't pick it up 60 | $("#move").click(); 61 | $("#lightbox").hide().data("dicom_attrs", null)[0].src=""; 62 | $("#series img").removeClass("selected"); 63 | $(this.el ).find("img").addClass("selected"); 64 | $("#pixel_spacing").empty(); 65 | $("#pixel_message").empty(); 66 | $("#window_info").empty(); 67 | this.model.instances.trigger("show"); 68 | $("#image_number").text("Image #1 of "+this.model.instances.length); 69 | } 70 | }); 71 | 72 | var InstanceStack = Backbone.View.extend({ 73 | el:"#instances", 74 | events: { 75 | "click img":"openInstance" 76 | }, 77 | initialize:function(){ 78 | _.bindAll(this); 79 | this.collection.bind("show", this.render); 80 | }, 81 | render:function(){ 82 | $(this.el).empty(); 83 | $(this.el).append(""); 84 | // We are reusing the same DOM for all instances, so we need to clear out anything 85 | // placed here by any previous instances 86 | $(this.el).unbind(); 87 | this.delegateEvents(); 88 | this.collection.models[0].trigger("render"); 89 | $("#instancepanel td:first-child img").click(); 90 | this.collection.each(function(element, index){ 91 | if (!index) return; 92 | element.trigger("render"); 93 | }); 94 | }, 95 | openInstance: function(event){ 96 | if ($("#measure").hasClass("selected")) $("#move").click(); 97 | var lightBox = $("#lightbox"); 98 | var m = $(event.target).data("model"); 99 | lightBox.data("dicom_attrs", null); 100 | var previousObj = Utils.queryString2Object(lightBox[0].src); 101 | var newObj = Utils.queryString2Object(event.target.src); 102 | // If lightBox is visible (meaning this is not the first instance opened for this series) 103 | // maintain ww/wl 104 | if (lightBox.is(":visible")){ 105 | if (previousObj.rows){ 106 | newObj.rows = previousObj.rows; 107 | } 108 | if (previousObj.windowWidth && previousObj.windowCenter){ 109 | newObj.windowWidth=previousObj.windowWidth; 110 | newObj.windowCenter=+previousObj.windowCenter; 111 | } 112 | }else{ 113 | lightBox.trigger("study/new"); 114 | // Show image at its native resolution 115 | lightBox.removeAttr("height"); 116 | lightBox.removeAttr("width"); 117 | } 118 | lightBox[0].src = [event.target.src.split("?")[0], jQuery.param(newObj).replace("252F", "2F")].join('?'); 119 | Utils.retrieveDicomAttributes(m, event.target.src); 120 | lightBox.data("model", m); 121 | lightBox.data("index", $.inArray(m, m.collection.models)); 122 | $("img",this.el).removeClass("selected"); 123 | $(event.target).addClass("selected"); 124 | $("#image_number").text("Image #" + ($.inArray(m, m.collection.models)+1) + " of " + m.collection.models.length ); 125 | $("#pixel_spacing").empty(); 126 | $("#pixel_message").empty(); 127 | } 128 | }); 129 | 130 | var InstanceView = Backbone.View.extend({ 131 | tagName:"td", 132 | className: "instance", 133 | initialize: function() { 134 | _.bindAll(this, "render"); 135 | this.template = _.template($("#instanceview").html()); 136 | this.model.bind("render",this.render); 137 | }, 138 | render: function(){ 139 | $("#instances tbody").append(this.el); 140 | var index = $.inArray(this.model, this.model.collection.models); 141 | $(this.el).html(this.template({wado_url:(Config.WADOHost === '/' ? '/' : (Config.WADOHost ? ((Config.WADOProt || "https") + "://" + Config.WADOHost+ (Config.WADOPort ? ":"+Config.WADOPort: "") +"/") : "" )) 142 | + Config.WADOPath + "?"+(Config.png ? "contentType=image%2Fpng&":"contentType=image%2Fjpeg&")+ "requestType=WADO&studyUID="+this.model.get("studyUID")+"&" 143 | + "seriesUID="+this.model.get("seriesUID") + "&" + "objectUID=" + this.model.get("uid") 144 | + "&rows="+Config.DefaultImgSize, 145 | size:Config.InstanceThumbNailSizePx})); 146 | $("img",this.el).data("model", this.model); 147 | this.model.attributes.url = $('img', this.el)[0].src; 148 | // This is not efficient, but this allows us to wrap tr around 149 | // each group of Config.ImagesPerRow as we build it. This view 150 | // should probably pass the collection, not the model 151 | if ((index % Config.ImagesPerRow === Config.ImagesPerRow - 1) 152 | || (index === this.model.collection.models.length)) { 153 | $(this.el).prevUntil("table,tr").add(this.el).wrapAll(""); 154 | } 155 | } 156 | }); 157 | 158 | return { 159 | SeriesView:SeriesView, 160 | InstanceView:InstanceView, 161 | InstanceStackView:InstanceStack}; 162 | }); 163 | -------------------------------------------------------------------------------- /client/js/libs/raphaelle.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Drag and drop for Raphael elements. 3 | * 4 | * Original Author: Gabe Hollombe. Rewritten several times over by Clifford Heath. 5 | * (c) Copyright. Subject to MIT License. 6 | * 7 | * You need to include jquery and Raphael first. 8 | * 9 | * When you call draggable() on any Raphael object, it becomes a handle that can start a drag. 10 | * A mousedown on the handle followed by enough motion causes a drag to start. 11 | * 12 | * The dragged object may be the handle itself, another object defined in the options to 13 | * draggable(), or an object returned from the handle's dragStart method (if defined). 14 | * 15 | * The drag proceeds by calling drag_obj.dragUpdate for each mouse motion, see below. 16 | * If no dragUpdate is defined, a Raphael translate() is used to provide simple motion. 17 | * 18 | * If the Escape key is pressed during the drag, the motion is reverted and the drag stops. 19 | * In this case, dragCancel will be called, see below. 20 | * 21 | * When a drag finished normally, dragFinish is called as defined below. 22 | * 23 | * Options: 24 | * drag_obj 25 | * A click on the handle will start a drag on this object. 26 | * Otherwise handle will be dragged, unless handle.dragStart() 27 | * returns a different draggable object. 28 | * right_button 29 | * unset means drag using either button, otherwise false/true means left/right only. 30 | * reluctance 31 | * Number of pixels of motion before a drag starts (default 3). 32 | * 33 | * Optional method on handle. The events are passed so you can see the modifier keys. 34 | * dragStart(x, y, mousedownevent, mousemoveevent) 35 | * called (after reluctance) with the mousedown event and canvas location. 36 | * Must return the object to drag. Good place to call toFront so the drag_obj doesn't hide. 37 | * 38 | * Optional methods on drag_obj. The event is passed so you can see the modifier keys. 39 | * dragUpdate(dragging_over, dx, dy, event) 40 | * dragging_over: the object under the cursor (after hiding drag_obj) 41 | * dx,dy: the number of units of motion 42 | * event: the mousemotion event 43 | * dragFinish(dropped_on, x, y, event) 44 | * dropped_on: the object under the cursor (after hiding drag_obj) 45 | * x,y: the page location 46 | * event: the mouseup event 47 | * dragCancel() 48 | * called if the drag is cancelled 49 | */ 50 | define(["jquery"], function(jQuery) { 51 | Raphael.el.draggable = function(options) { 52 | var handle = this; // The object you click on 53 | if (!options) options = {}; 54 | // If you define drag_obj to be null, you must provide a dragStart that returns one: 55 | var drag_obj = typeof options.drag_obj !== 'undefined' ? options.drag_obj : handle; 56 | 57 | // Set right_button to true for right-click dragging only, to false for left-click only. Otherwise you get both. 58 | var right_button = options.right_button; 59 | 60 | // Check that this is an ok thing to even think about doing 61 | if (!(handle.node && handle.node.raphael)) { 62 | alert(handle+' is not a Raphael object so you can\'t make it draggable'); 63 | return; 64 | } 65 | if (drag_obj && !(drag_obj.node && drag_obj.node.raphael)) { 66 | alert(drag_obj+' is not a Raphael object so you can\'t make it draggable'); 67 | return; 68 | } 69 | 70 | // options.reluctance is the number of pixels of motion before a drag will start: 71 | var reluctance = options.reluctance; 72 | if (typeof reluctance == 'undefined') reluctance = 3; 73 | 74 | var skip_click; 75 | var mousedown = function(event) { 76 | if (typeof right_button != 'undefined' && (right_button === false) === (event.button > 1)) 77 | return true; 78 | 79 | skip_click = false; // Used to skip a click after dragging 80 | var started = false; // Has the drag started? 81 | var start_event = event; // The starting mousedown 82 | var last_x = event.clientX, last_y = event.clientY; // Where did we move from last? 83 | 84 | // Figure out what object (other than drag_obj) is under the pointer 85 | var over = function(event) { 86 | var paper = handle.paper || drag_obj.paper; 87 | if (!paper) return null; // Something was deleted 88 | if (drag_obj) drag_obj.hide(); 89 | var dragging_over = document.elementFromPoint(event.clientX, event.clientY); 90 | if ($.browser.opera && dragging_over.tagName === 'svg') { 91 | // Opera's elementFromPoint always returns the SVG object. 92 | var svg = paper.canvas; 93 | var so = $(svg).offset(); 94 | // var so = canvas_offset(svg); 95 | var sr = svg.createSVGRect(); 96 | sr.x = event.clientX-so.left; 97 | sr.y = event.clientY-so.top; 98 | sr.width = sr.height = 1; 99 | var hits = svg.getIntersectionList(sr, null); 100 | if (hits.length > 0) 101 | { 102 | dragging_over = hits[hits.length-1]; 103 | // drag_obj.hide() probably hasn't taken effect yet. Hope it's not a compound object: 104 | if (dragging_over == drag_obj.node && hits.length > 1) 105 | dragging_over = hits[hits.length-2]; 106 | } 107 | } 108 | if (drag_obj) drag_obj.show(); 109 | if (!dragging_over) 110 | return null; 111 | if (dragging_over.nodeType == 3) 112 | return dragging_over.parentNode; // Safari/Opera 113 | if (dragging_over.tagName != 'svg' && dragging_over == paper.canvas.parentNode) 114 | return paper.canvas; // Safari 115 | if (dragging_over == paper.canvas) 116 | return dragging_over; 117 | if (!dragging_over.raphael) 118 | return dragging_over.parentNode; // A tspan inside a Raphael text object perhaps? 119 | return dragging_over; 120 | }; 121 | 122 | var canvas_offset = function(canvas) { 123 | if (!$.browser.opera && canvas.getClientRects) 124 | { 125 | // This works around a bug in Chrome 8.0.552.215 126 | var cr = canvas.getClientRects()[0]; 127 | return {top: cr.top, left: cr.left }; 128 | } 129 | return jQuery(canvas).offset(); 130 | } 131 | 132 | var mousemove = function(event) { 133 | // REVISIT: Need to use some_svg_element_node.getScreenCTM() with SVG zooming comes into play 134 | var delta_x = event.clientX-last_x; 135 | var delta_y = event.clientY-last_y; 136 | 137 | if (!started && (delta_x>=reluctance || delta_x<=-reluctance || delta_y>=reluctance || delta_y<=-reluctance)) { 138 | if (handle.dragStart) { 139 | var position = canvas_offset(handle.paper.canvas); 140 | 141 | var o = handle.dragStart(event.clientX-delta_x-position.left, event.clientY-delta_y-position.top, start_event, event); 142 | if (!o) return false; // Don't start the drag yet if told not to 143 | drag_obj = o; 144 | } 145 | started = true; 146 | skip_click = true; 147 | } 148 | if (!started || !drag_obj) return false; 149 | 150 | var dragging_over = over(event); 151 | // console.log("Move "+drag_obj.node.id+" over "+dragging_over.id+" to X="+event.clientX+", Y="+event.clientY); 152 | var update = drag_obj.dragUpdate ? drag_obj.dragUpdate : function(o, dx, dy, e) { drag_obj.translate(dx, dy); }; 153 | update(dragging_over, delta_x, delta_y, event); 154 | handle.paper.safari(); 155 | last_x = event.clientX; 156 | last_y = event.clientY; 157 | return false; 158 | }; 159 | 160 | if (reluctance == 0 && handle.dragStart) { 161 | var o = handle.dragStart(0, 0, event, event); 162 | if (!o) return false; 163 | drag_obj = o; 164 | started = true; 165 | skip_click = true; 166 | } 167 | 168 | var revert; 169 | var cancel; 170 | 171 | // Process keyboard input so we can cancel 172 | var keydown = function(event) { 173 | if (event.keyCode == 27) { // Escape 174 | revert(event); 175 | if (drag_obj.dragCancel) 176 | drag_obj.dragCancel(); 177 | cancel(); 178 | return false; 179 | } 180 | return true; 181 | }; 182 | 183 | // Revert to starting location 184 | revert = function(event) { 185 | if (!started) return; 186 | if (drag_obj && drag_obj.dragUpdate) 187 | drag_obj.dragUpdate(null, start_event.clientX-last_x, start_event.clientY-last_y, event); 188 | started = false; // Sometimes get the same event twice. 189 | }; 190 | 191 | // The drag has ended, deal with it. 192 | var mouseup = function(event) { 193 | if (started) { 194 | var dropped_on = over(event); 195 | if (drag_obj && drag_obj.dragFinish) { 196 | var position = canvas_offset(handle.paper.canvas); 197 | 198 | drag_obj.dragFinish(dropped_on, event.clientX-position.left, event.clientY-position.top, event); 199 | } 200 | cancel(); 201 | return true; // Don't let it bubble 202 | } 203 | cancel(); 204 | return true; 205 | }; 206 | 207 | // Undo event bindings after the drag 208 | handle.originalDraggableNode = handle.node; 209 | cancel = function() { 210 | $(document).unbind('mouseup', mouseup); 211 | $(document).unbind('mousemove', mousemove); 212 | $(document).unbind('keydown', keydown); 213 | if ($.browser.msie) { 214 | // Rebind the mousedown if it got lost when the node was recreated: 215 | if (handle.originalDraggableNode != handle.node) 216 | $(handle.node).bind('mousedown', mousedown); 217 | handle.originalDraggableNode = handle.node; 218 | } 219 | handle.paper.safari(); 220 | 221 | started = false; 222 | }; 223 | 224 | // Bind the appropriate events for the duration of the drag: 225 | $(document).bind('keydown', keydown); 226 | $(document).bind('mousemove', mousemove); 227 | $(document).bind('mouseup', mouseup); 228 | 229 | event.stopImmediatePropagation(); // Ensure that whatever drag we start, there's only one! 230 | return false; 231 | }; 232 | var click = function(event) { 233 | if (skip_click) 234 | event.stopImmediatePropagation(); 235 | skip_click = false; // Used to skip a click after dragging 236 | return true; 237 | }; 238 | $(handle.node).bind('mousedown', mousedown); 239 | $(handle.node).bind('click', click); // Bind click now so that we can stop other click handlers 240 | }; 241 | }); 242 | -------------------------------------------------------------------------------- /server/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http import HttpResponse, Http404 3 | from django.template.loader import get_template 4 | from django.template import Context 5 | from django.conf import settings 6 | import cStringIO 7 | import requests 8 | import gdcm 9 | import json 10 | import dicom 11 | 12 | # Some DICOM SOP class constants 13 | CT = "1.2.840.10008.5.1.4.1.1.2" 14 | MR = "1.2.840.10008.5.1.4.1.1.4" 15 | XA = "1.2.840.10008.5.1.4.1.1.12.1" 16 | CR = "1.2.840.10008.5.1.4.1.1.1" 17 | 18 | STUDY_IUID = (0x20,0xD) 19 | STUDY_DESCR = (0x8, 0x1030) 20 | SERIES_IUID = (0x20,0xE) 21 | SERIES_DESCR = (0x8, 0x103E) 22 | SOP_CLASS_UID = (0x8,0x16) 23 | PIXEL_SPACING = (0x28,0x30) 24 | IMAGER_PIXEL_SPACING = (0x18,0x1164) 25 | WINDOW_CENTER = (0x28,0x1050) 26 | WINDOW_WIDTH = (0x28, 0x1051) 27 | CALIBRATION_TYPE = (0x28,0x402) 28 | CALIBRATION_DESCR = (0x28,0x404) 29 | 30 | WADO_URL = "%s://%s:%d/%s" % (settings.SC_WADO_PROT, settings.SC_WADO_SERVER, settings.SC_WADO_PORT, 31 | settings.SC_WADO_PATH) 32 | 33 | def app_root(request): 34 | document_template = get_template("index.html") 35 | document = document_template.render(Context({})) 36 | document = document.replace('var STATIC_URL = "";','var STATIC_URL = "%s";' % settings.STATIC_URL) 37 | return HttpResponse(document) 38 | 39 | def study(request, study_iuid): 40 | # Patient Name 41 | response = {} 42 | study_iuid_tag = gdcm.Tag(0x20,0xD) 43 | study_iuid_element = gdcm.DataElement(study_iuid_tag) 44 | study_descr_tag = gdcm.Tag(0x8,0x1030) 45 | study_descr_element = gdcm.DataElement(study_descr_tag) 46 | study_iuid_element.SetByteValue(str(study_iuid), gdcm.VL(len(study_iuid))) 47 | ds = gdcm.DataSet() 48 | ds.Insert(study_iuid_element) 49 | ds.Insert(study_descr_element) 50 | cnf = gdcm.CompositeNetworkFunctions() 51 | theQuery = cnf.ConstructQuery(gdcm.eStudyRootType, gdcm.eStudy, ds) 52 | 53 | # prepare the variable for output 54 | ret = gdcm.DataSetArrayType() 55 | # Execute the C-FIND query 56 | cnf.CFind(settings.SC_DICOM_SERVER, settings.SC_DICOM_PORT, 57 | theQuery, ret, 'GDCM_PYTHON', settings.AET) 58 | 59 | response["description"] = str(ret[0].GetDataElement(study_descr_tag).GetValue()) 60 | 61 | ds = gdcm.DataSet() 62 | 63 | series_descr_tag = gdcm.Tag(0x8,0x103E) 64 | series_descr_element = gdcm.DataElement(series_descr_tag) 65 | series_iuid_tag = gdcm.Tag(0x20,0xE) 66 | series_iuid_element = gdcm.DataElement(series_iuid_tag) 67 | series_number_tag = gdcm.Tag(0x20,0x11) 68 | series_number_element = gdcm.DataElement(series_number_tag) 69 | 70 | ds.Insert(study_iuid_element) 71 | ds.Insert(series_descr_element) 72 | ds.Insert(series_iuid_element) 73 | ds.Insert(series_number_element) 74 | 75 | series_query = cnf.ConstructQuery(gdcm.eStudyRootType, gdcm.eSeries, ds) 76 | ret = gdcm.DataSetArrayType() 77 | cnf.CFind(settings.SC_DICOM_SERVER, settings.SC_DICOM_PORT, series_query, 78 | ret, 'GDCM_PYTHON', settings.AET) 79 | 80 | sorted_ret = sorted(ret, key = lambda x: int(str(x.GetDataElement(series_number_tag).GetValue()))) 81 | 82 | response["series"] = [{"description":str(x.GetDataElement(series_descr_tag).GetValue()), 83 | "uid": str(x.GetDataElement(series_iuid_tag).GetValue())} for x in sorted_ret] 84 | 85 | 86 | json_response = json.dumps(response) 87 | 88 | if request.GET.has_key('callback'): 89 | json_response = "(function(){%s(%s);})();" % (request.GET['callback'], json_response) 90 | 91 | return HttpResponse(json_response, content_type="application/json") 92 | 93 | 94 | def series(request, series_iuid): 95 | 96 | series_iuid_tag = gdcm.Tag(0x20,0xE) 97 | series_iuid_element = gdcm.DataElement(series_iuid_tag) 98 | series_iuid_element.SetByteValue(str(series_iuid), gdcm.VL(len(series_iuid))) 99 | instance_uid_tag = gdcm.Tag(0x8, 0x18) 100 | instance_uid_element = gdcm.DataElement(instance_uid_tag) 101 | instance_number_tag = gdcm.Tag(0x20,0x13) 102 | instance_number_element = gdcm.DataElement(instance_number_tag) 103 | 104 | ds = gdcm.DataSet() 105 | ds.Insert(series_iuid_element) 106 | ds.Insert(instance_uid_element) 107 | ds.Insert(instance_number_element) 108 | 109 | cnf = gdcm.CompositeNetworkFunctions() 110 | 111 | instance_query = cnf.ConstructQuery(gdcm.eStudyRootType, gdcm.eImage, ds) 112 | ret = gdcm.DataSetArrayType() 113 | cnf.CFind(settings.SC_DICOM_SERVER, settings.SC_DICOM_PORT, instance_query, 114 | ret, 'GDCM_PYTHON', settings.AET) 115 | 116 | sorted_ret = sorted(ret, key = lambda x: int(str(x.GetDataElement(instance_number_tag).GetValue()))) 117 | response = [str(x.GetDataElement(instance_uid_tag).GetValue()) for x in sorted_ret] 118 | 119 | json_response = json.dumps(response) 120 | 121 | if request.GET.has_key('callback'): 122 | json_response = "(function(){%s(%s);})();" % (request.GET['callback'], json_response) 123 | 124 | return HttpResponse(json_response, content_type="application/json") 125 | 126 | # Convenience function to get pixel calibration details 127 | def calibrationDetails(dcm_obj): 128 | details = "Not available." 129 | calibration_type = None 130 | calibration_descr = None 131 | calibration_type = dcm_obj[CALIBRATION_TYPE].value if \ 132 | CALIBRATION_TYPE in dcm_obj else None 133 | calibration_descr = dcm_obj[CALIBRATION_DESCR].value if \ 134 | CALIBRATION_DESCR in dcm_obj else None 135 | 136 | if calibration_type and calibration_descr: 137 | details = "%s - %s" % (calibration_type, calibration_descr) 138 | elif calibration_type or calibration_descr: 139 | details = calibration_type or calibration_descr 140 | 141 | return details 142 | 143 | 144 | # Proxy to WADO server that only allows jpeg or png 145 | def wado(request): 146 | if request.GET.has_key('contentType') and (request.GET['contentType'] == 'image/jpeg' or request.GET['contentType'] == 'image/png'): 147 | r = requests.get(WADO_URL, params=request.GET) 148 | data = r.content 149 | return HttpResponse(data, content_type=request.GET['contentType']) 150 | return Http404 151 | 152 | def instance(request, instance_uid): 153 | 154 | payload = {'contentType': 'application/dicom', 155 | 'seriesUID':'', 156 | 'studyUID' :'', 157 | 'objectUID': instance_uid, 158 | 'requestType':'WADO', 159 | 'transferSyntax':'1.2.840.10008.1.2.2'} # explicit big endian 160 | # explicit little endian is '1.2.840.10008.1.2.1' 161 | r = requests.get(WADO_URL, params=payload) 162 | data = r.content 163 | file_like = cStringIO.StringIO(data) 164 | dcm_obj = dicom.read_file(file_like) 165 | file_like.close() 166 | 167 | modality_type = None 168 | modality_type = dcm_obj[SOP_CLASS_UID].value if SOP_CLASS_UID in dcm_obj else None 169 | spacing = None 170 | xSpacing = None 171 | ySpacing = None 172 | pixel_attr = None 173 | pixel_message = None 174 | response = {} 175 | if modality_type in [MR, CT]: 176 | spacing = dcm_obj[PIXEL_SPACING].value if PIXEL_SPACING in dcm_obj else None 177 | pixel_attr = PIXEL_SPACING 178 | elif modality_type in [CR, XA]: 179 | # The following logic is taken from CP 586 180 | pixel_spacing = dcm_obj[PIXEL_SPACING].value if PIXEL_SPACING in dcm_obj else None 181 | imager_spacing = dcm_obj[IMAGER_PIXEL_SPACING].value if IMAGER_PIXEL_SPACING in dcm_obj else None 182 | if pixel_spacing: 183 | if imager_spacing: 184 | if pixel_spacing == imager_spacing: 185 | # Both attributes are present 186 | spacing = imager_spacing 187 | pixel_attr = IMAGER_PIXEL_SPACING 188 | pixel_message = "Measurements are at the detector plane." 189 | else: 190 | # Using Pixel Spacing 191 | spacing = pixel_spacing 192 | pixel_attr = PIXEL_SPACING 193 | pixel_message = "Measurement has been calibrated, details = %s " % \ 194 | calibrationDetails(dcm_obj) 195 | else: 196 | # Only Pixel Spacing was specified 197 | spacing = pixel_spacing 198 | pixel_attr = PIXEL_SPACING 199 | pixel_message = "Warning measurement may have been calibrated, details: %s. It is not clear" + \ 200 | " what this measurement represents." % calibrationDetails(dcm_obj) 201 | elif imager_spacing: 202 | spacing = imager_spacing 203 | pixel_attr = IMAGER_PIXEL_SPACING 204 | pixel_message = "Measurements are at the detector plane." 205 | 206 | # Build up the response 207 | response["windowCenter"] = None 208 | response["windowWidth"] = None 209 | 210 | if WINDOW_CENTER in dcm_obj: 211 | if dcm_obj[WINDOW_CENTER].VM > 1: 212 | response["windowCenter"] = int(dcm_obj[WINDOW_CENTER].value[0]) 213 | else: 214 | response["windowCenter"] = int(dcm_obj[WINDOW_CENTER].value) 215 | 216 | if WINDOW_WIDTH in dcm_obj: 217 | if dcm_obj[WINDOW_WIDTH].VM > 1: 218 | response["windowWidth"] = int(dcm_obj[WINDOW_WIDTH].value[0]) 219 | else: 220 | response["windowWidth"] = int(dcm_obj[WINDOW_WIDTH].value) 221 | 222 | # Pixel spacing attributes can contain two values packed like this: 223 | # x//y 224 | if spacing: 225 | xSpacing = ySpacing = spacing[0] 226 | if len(spacing) > 1: 227 | ySpacing = spacing[1] 228 | 229 | response["xSpacing"] = xSpacing 230 | response["ySpacing"] = ySpacing 231 | response["pixelMessage"] = pixel_message 232 | response["pixelAttr"] = pixel_attr 233 | response["nativeRows"] = dcm_obj.Rows 234 | response["nativeCols"] = dcm_obj.Columns 235 | response["studyDescr"] = dcm_obj[STUDY_DESCR].value if STUDY_DESCR in dcm_obj else None 236 | response["seriesDescr"] = dcm_obj[SERIES_DESCR].value if SERIES_DESCR in dcm_obj else None 237 | response["objectUID"] = instance_uid 238 | json_response = json.dumps(response) 239 | 240 | if request.GET.has_key('callback'): 241 | json_response = "(function(){%s(%s);})();" % (request.GET['callback'], json_response) 242 | return HttpResponse(json_response, content_type="application/json") 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /client/js/libs/underscore-min.js: -------------------------------------------------------------------------------- 1 | // Underscore.js 1.2.1 2 | // (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. 3 | // Underscore is freely distributable under the MIT license. 4 | // Portions of Underscore are inspired or borrowed from Prototype, 5 | // Oliver Steele's Functional, and John Resig's Micro-Templating. 6 | // For all details and documentation: 7 | // http://documentcloud.github.com/underscore 8 | (function(){function u(a,c,d){if(a===c)return a!==0||1/a==1/c;if(a==null||c==null)return a===c;if(a._chain)a=a._wrapped;if(c._chain)c=c._wrapped;if(b.isFunction(a.isEqual))return a.isEqual(c);if(b.isFunction(c.isEqual))return c.isEqual(a);var e=typeof a;if(e!=typeof c)return false;if(!a!=!c)return false;if(b.isNaN(a))return b.isNaN(c);var g=b.isString(a),f=b.isString(c);if(g||f)return g&&f&&String(a)==String(c);g=b.isNumber(a);f=b.isNumber(c);if(g||f)return g&&f&&+a==+c;g=b.isBoolean(a);f=b.isBoolean(c); 9 | if(g||f)return g&&f&&+a==+c;g=b.isDate(a);f=b.isDate(c);if(g||f)return g&&f&&a.getTime()==c.getTime();g=b.isRegExp(a);f=b.isRegExp(c);if(g||f)return g&&f&&a.source==c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase;if(e!="object")return false;if(a.length!==c.length)return false;if(a.constructor!==c.constructor)return false;for(e=d.length;e--;)if(d[e]==a)return true;d.push(a);var e=0,g=true,h;for(h in a)if(m.call(a,h)&&(e++,!(g=m.call(c,h)&&u(a[h],c[h],d))))break;if(g){for(h in c)if(m.call(c, 10 | h)&&!e--)break;g=!e}d.pop();return g}var r=this,F=r._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,G=k.unshift,l=p.toString,m=p.hasOwnProperty,v=k.forEach,w=k.map,x=k.reduce,y=k.reduceRight,z=k.filter,A=k.every,B=k.some,q=k.indexOf,C=k.lastIndexOf,p=Array.isArray,H=Object.keys,s=Function.prototype.bind,b=function(a){return new n(a)};if(typeof exports!=="undefined"){if(typeof module!=="undefined"&&module.exports)exports=module.exports=b;exports._=b}else typeof define==="function"&&define.amd? 11 | define("underscore",function(){return b}):r._=b;b.VERSION="1.2.1";var j=b.each=b.forEach=function(a,c,b){if(a!=null)if(v&&a.forEach===v)a.forEach(c,b);else if(a.length===+a.length)for(var e=0,g=a.length;e=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a))return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;bd?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,g=a.length;e< 17 | g;){var f=e+g>>1;d(a[f])=0})})};b.difference=function(a,c){return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return function(){if(--a<1)return b.apply(this,arguments)}};b.keys=H||function(a){if(a!==Object(a))throw new TypeError("Invalid object"); 24 | var b=[],d;for(d in a)m.call(a,d)&&(b[b.length]=d);return b};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)b[d]!==void 0&&(a[d]=b[d])});return a};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({}, 25 | a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return u(a,b,[])};b.isEmpty=function(a){if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(m.call(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=l.call(arguments)=="[object Arguments]"?function(a){return l.call(a)=="[object Arguments]"}:function(a){return!(!a||!m.call(a, 26 | "callee"))};b.isFunction=function(a){return l.call(a)=="[object Function]"};b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a=== 27 | void 0};b.noConflict=function(){r._=F;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.mixin=function(a){j(b.functions(a),function(c){I(c,b[c]=a[c])})};var J=0;b.uniqueId=function(a){var b=J++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g, 28 | interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};b.template=function(a,c){var d=b.templateSettings,d="var __p=[],print=function(){__p.push.apply(__p,arguments);};with(obj||{}){__p.push('"+a.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(d.escape,function(a,b){return"',_.escape("+b.replace(/\\'/g,"'")+"),'"}).replace(d.interpolate,function(a,b){return"',"+b.replace(/\\'/g,"'")+",'"}).replace(d.evaluate||null,function(a,b){return"');"+b.replace(/\\'/g,"'").replace(/[\r\n\t]/g," ")+"__p.push('"}).replace(/\r/g, 29 | "\\r").replace(/\n/g,"\\n").replace(/\t/g,"\\t")+"');}return __p.join('');",d=new Function("obj",d);return c?d(c):d};var n=function(a){this._wrapped=a};b.prototype=n.prototype;var t=function(a,c){return c?b(a).chain():a},I=function(a,c){n.prototype[a]=function(){var a=i.call(arguments);G.call(a,this._wrapped);return t(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];n.prototype[a]=function(){b.apply(this._wrapped,arguments);return t(this._wrapped, 30 | this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];n.prototype[a]=function(){return t(b.apply(this._wrapped,arguments),this._chain)}});n.prototype.chain=function(){this._chain=true;return this};n.prototype.value=function(){return this._wrapped}})(); 31 | -------------------------------------------------------------------------------- /client/js/libs/require.js: -------------------------------------------------------------------------------- 1 | /* 2 | RequireJS 1.0.1 Copyright (c) 2010-2011, The Dojo Foundation All Rights Reserved. 3 | Available via the MIT or new BSD license. 4 | see: http://github.com/jrburke/requirejs for details 5 | */ 6 | var requirejs,require,define; 7 | (function(){function J(b){return M.call(b)==="[object Function]"}function E(b){return M.call(b)==="[object Array]"}function Z(b,c,i){for(var j in c)if(!(j in K)&&(!(j in b)||i))b[j]=c[j];return d}function N(b,c,d){b=Error(c+"\nhttp://requirejs.org/docs/errors.html#"+b);if(d)b.originalError=d;return b}function $(b,c,d){var j,l,q;for(j=0;q=c[j];j++){q=typeof q==="string"?{name:q}:q;l=q.location;if(d&&(!l||l.indexOf("/")!==0&&l.indexOf(":")===-1))l=d+"/"+(l||q.name);b[q.name]={name:q.name,location:l|| 8 | q.name,main:(q.main||"main").replace(da,"").replace(aa,"")}}}function V(b,c){b.holdReady?b.holdReady(c):c?b.readyWait+=1:b.ready(!0)}function ea(b){function c(a,h){var e,s;if(a&&a.charAt(0)==="."&&h){p.pkgs[h]?h=[h]:(h=h.split("/"),h=h.slice(0,h.length-1));e=a=h.concat(a.split("/"));var b;for(s=0;b=e[s];s++)if(b===".")e.splice(s,1),s-=1;else if(b==="..")if(s===1&&(e[2]===".."||e[0]===".."))break;else s>0&&(e.splice(s-1,2),s-=2);s=p.pkgs[e=a[0]];a=a.join("/");s&&a===e+"/"+s.main&&(a=e)}return a}function i(a, 9 | h){var e=a?a.indexOf("!"):-1,b=null,d=h?h.name:null,f=a,k,i;e!==-1&&(b=a.substring(0,e),a=a.substring(e+1,a.length));b&&(b=c(b,d));a&&(b?k=(e=n[b])&&e.normalize?e.normalize(a,function(a){return c(a,d)}):c(a,d):(k=c(a,d),i=E[k],i||(i=g.nameToUrl(k,null,h),E[k]=i)));return{prefix:b,name:k,parentMap:h,url:i,originalName:f,fullName:b?b+"!"+(k||""):k}}function j(){var a=!0,h=p.priorityWait,e,b;if(h){for(b=0;e=h[b];b++)if(!t[e]){a=!1;break}a&&delete p.priorityWait}return a}function l(a,h,e){return function(){var b= 10 | ga.call(arguments,0),c;if(e&&J(c=b[b.length-1]))c.__requireJsBuild=!0;b.push(h);return a.apply(null,b)}}function q(a,h){var e=l(g.require,a,h);Z(e,{nameToUrl:l(g.nameToUrl,a),toUrl:l(g.toUrl,a),defined:l(g.requireDefined,a),specified:l(g.requireSpecified,a),isBrowser:d.isBrowser});return e}function o(a){var h,e,b;b=a.callback;var c=a.map,f=c.fullName,k=a.deps,fa=a.listeners;if(b&&J(b)){if(p.catchError.define)try{e=d.execCb(f,a.callback,k,n[f])}catch(j){h=j}else e=d.execCb(f,a.callback,k,n[f]);if(f)a.cjsModule&& 11 | a.cjsModule.exports!==void 0?e=n[f]=a.cjsModule.exports:e===void 0&&a.usingExports?e=n[f]:(n[f]=e,F[f]&&(Q[f]=!0))}else f&&(e=n[f]=b,F[f]&&(Q[f]=!0));if(D[a.id])delete D[a.id],a.isDone=!0,g.waitCount-=1,g.waitCount===0&&(I=[]);delete R[f];if(d.onResourceLoad&&!a.placeholder)d.onResourceLoad(g,c,a.depArray);if(h)return e=(f?i(f).url:"")||h.fileName||h.sourceURL,b=h.moduleTree,h=N("defineerror",'Error evaluating module "'+f+'" at location "'+e+'":\n'+h+"\nfileName:"+e+"\nlineNumber: "+(h.lineNumber|| 12 | h.line),h),h.moduleName=f,h.moduleTree=b,d.onError(h);for(h=0;b=fa[h];h++)b(e)}function r(a,h){return function(b){a.depDone[h]||(a.depDone[h]=!0,a.deps[h]=b,a.depCount-=1,a.depCount||o(a))}}function v(a,h){var b=h.map,c=b.fullName,i=b.name,f=L[a]||(L[a]=n[a]),k;if(!h.loading)h.loading=!0,k=function(a){h.callback=function(){return a};o(h);t[h.id]=!0;x()},k.fromText=function(a,h){var b=O;t[a]=!1;g.scriptCount+=1;g.fake[a]=!0;b&&(O=!1);d.exec(h);b&&(O=!0);g.completeLoad(a)},c in n?k(n[c]):f.load(i,q(b.parentMap, 13 | !0),k,p)}function w(a){D[a.id]||(D[a.id]=a,I.push(a),g.waitCount+=1)}function C(a){this.listeners.push(a)}function u(a,b){var e=a.fullName,c=a.prefix,d=c?L[c]||(L[c]=n[c]):null,f,k;e&&(f=R[e]);if(!f&&(k=!0,f={id:(c&&!d?M++ +"__p@:":"")+(e||"__r@"+M++),map:a,depCount:0,depDone:[],depCallbacks:[],deps:[],listeners:[],add:C},z[f.id]=!0,e&&(!c||L[c])))R[e]=f;c&&!d?(e=u(i(c),!0),e.add(function(){var b=i(a.originalName,a.parentMap),b=u(b,!0);f.placeholder=!0;b.add(function(a){f.callback=function(){return a}; 14 | o(f)})})):k&&b&&(t[f.id]=!1,g.paused.push(f),w(f));return f}function y(a,b,e,c){var a=i(a,c),d=a.name,f=a.fullName,k=u(a),j=k.id,l=k.deps,m;if(f){if(f in n||t[j]===!0||f==="jquery"&&p.jQuery&&p.jQuery!==e().fn.jquery)return;z[j]=!0;t[j]=!0;f==="jquery"&&e&&S(e())}k.depArray=b;k.callback=e;for(e=0;e0)){if(p.priorityWait)if(j())x();else return;for(i in t)if(!(i in K)&&(e=!0,!t[i]))if(b)a+=i+" ";else{c=!0;break}if(e||g.waitCount){if(b&&a)return i=N("timeout","Load timeout for modules: "+a),i.requireType="timeout",i.requireModules=a,d.onError(i);if(c||g.scriptCount){if((G||ba)&&!W)W=setTimeout(function(){W=0;B()},50)}else{if(g.waitCount){for(H=0;a=I[H];H++)A(a,{});g.paused.length&&x();X<5&&(X+=1,B())}X=0;d.checkReadyState()}}}} 17 | var g,x,p={waitSeconds:7,baseUrl:"./",paths:{},pkgs:{},catchError:{}},P=[],z={require:!0,exports:!0,module:!0},E={},n={},t={},D={},I=[],T={},M=0,R={},L={},F={},Q={},Y=0;S=function(a){if(!g.jQuery&&(a=a||(typeof jQuery!=="undefined"?jQuery:null))&&!(p.jQuery&&a.fn.jquery!==p.jQuery)&&("holdReady"in a||"readyWait"in a))if(g.jQuery=a,m(["jquery",[],function(){return jQuery}]),g.scriptCount)V(a,!0),g.jQueryIncremented=!0};x=function(){var a,b,e,c,i,f;Y+=1;if(g.scriptCount<=0)g.scriptCount=0;for(;P.length;)if(a= 18 | P.shift(),a[0]===null)return d.onError(N("mismatch","Mismatched anonymous define() module: "+a[a.length-1]));else m(a);if(!p.priorityWait||j())for(;g.paused.length;){i=g.paused;g.pausedCount+=i.length;g.paused=[];for(c=0;a=i[c];c++)b=a.map,e=b.url,f=b.fullName,b.prefix?v(b.prefix,a):!T[e]&&!t[f]&&(d.load(g,f,e),T[e]=!0);g.startTime=(new Date).getTime();g.pausedCount-=i.length}Y===1&&B();Y-=1};g={contextName:b,config:p,defQueue:P,waiting:D,waitCount:0,specified:z,loaded:t,urlMap:E,urlFetched:T,scriptCount:0, 19 | defined:n,paused:[],pausedCount:0,plugins:L,needFullExec:F,fake:{},fullExec:Q,managerCallbacks:R,makeModuleMap:i,normalize:c,configure:function(a){var b,e,c;a.baseUrl&&a.baseUrl.charAt(a.baseUrl.length-1)!=="/"&&(a.baseUrl+="/");b=p.paths;c=p.pkgs;Z(p,a,!0);if(a.paths){for(e in a.paths)e in K||(b[e]=a.paths[e]);p.paths=b}if((b=a.packagePaths)||a.packages){if(b)for(e in b)e in K||$(c,b[e],e);a.packages&&$(c,a.packages);p.pkgs=c}if(a.priority)e=g.requireWait,g.requireWait=!1,g.takeGlobalQueue(),x(), 20 | g.require(a.priority),x(),g.requireWait=e,p.priorityWait=a.priority;if(a.deps||a.callback)g.require(a.deps||[],a.callback)},requireDefined:function(a,b){return i(a,b).fullName in n},requireSpecified:function(a,b){return i(a,b).fullName in z},require:function(a,c,e){if(typeof a==="string"){if(J(c))return d.onError(N("requireargs","Invalid require call"));if(d.get)return d.get(g,a,c);c=i(a,c);a=c.fullName;return!(a in n)?d.onError(N("notloaded","Module name '"+c.fullName+"' has not been loaded yet for context: "+ 21 | b)):n[a]}(a&&a.length||c)&&y(null,a,c,e);if(!g.requireWait)for(;!g.scriptCount&&g.paused.length;)g.takeGlobalQueue(),x();return g.require},takeGlobalQueue:function(){U.length&&(ha.apply(g.defQueue,[g.defQueue.length-1,0].concat(U)),U=[])},completeLoad:function(a){var b;for(g.takeGlobalQueue();P.length;)if(b=P.shift(),b[0]===null){b[0]=a;break}else if(b[0]===a)break;else m(b),b=null;b?m(b):m([a,[],a==="jquery"&&typeof jQuery!=="undefined"?function(){return jQuery}:null]);S();d.isAsync&&(g.scriptCount-= 22 | 1);x();d.isAsync||(g.scriptCount-=1)},toUrl:function(a,b){var c=a.lastIndexOf("."),d=null;c!==-1&&(d=a.substring(c,a.length),a=a.substring(0,c));return g.nameToUrl(a,d,b)},nameToUrl:function(a,b,e){var i,j,f,k,l=g.config,a=c(a,e&&e.fullName);if(d.jsExtRegExp.test(a))b=a+(b?b:"");else{i=l.paths;j=l.pkgs;e=a.split("/");for(k=e.length;k>0;k--)if(f=e.slice(0,k).join("/"),i[f]){e.splice(0,k,i[f]);break}else if(f=j[f]){a=a===f.name?f.location+"/"+f.main:f.location;e.splice(0,k,a);break}b=e.join("/")+(b|| 23 | ".js");b=(b.charAt(0)==="/"||b.match(/^\w+:/)?"":l.baseUrl)+b}return l.urlArgs?b+((b.indexOf("?")===-1?"?":"&")+l.urlArgs):b}};g.jQueryCheck=S;g.resume=x;return g}function ia(){var b,c,d;if(m&&m.readyState==="interactive")return m;b=document.getElementsByTagName("script");for(c=b.length-1;c>-1&&(d=b[c]);c--)if(d.readyState==="interactive")return m=d;return null}var ja=/(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg,ka=/require\(\s*["']([^'"\s]+)["']\s*\)/g,da=/^\.\//,aa=/\.js$/,M=Object.prototype.toString, 24 | r=Array.prototype,ga=r.slice,ha=r.splice,G=!!(typeof window!=="undefined"&&navigator&&document),ba=!G&&typeof importScripts!=="undefined",la=G&&navigator.platform==="PLAYSTATION 3"?/^complete$/:/^(complete|loaded)$/,ca=typeof opera!=="undefined"&&opera.toString()==="[object Opera]",K={},u={},U=[],m=null,X=0,O=!1,d,r={},I,w,y,z,v,A,B,H,C,S,W;if(typeof define==="undefined"){if(typeof requirejs!=="undefined")if(J(requirejs))return;else r=requirejs,requirejs=void 0;typeof require!=="undefined"&&!J(require)&& 25 | (r=require,require=void 0);d=requirejs=function(b,c,d){var j="_",l;!E(b)&&typeof b!=="string"&&(l=b,E(c)?(b=c,c=d):b=[]);if(l&&l.context)j=l.context;d=u[j]||(u[j]=ea(j));l&&d.configure(l);return d.require(b,c)};d.config=function(b){return d(b)};require||(require=d);d.toUrl=function(b){return u._.toUrl(b)};d.version="1.0.1";d.jsExtRegExp=/^\/|:|\?|\.js$/;w=d.s={contexts:u,skipAsync:{}};if(d.isAsync=d.isBrowser=G)if(y=w.head=document.getElementsByTagName("head")[0],z=document.getElementsByTagName("base")[0])y= 26 | w.head=z.parentNode;d.onError=function(b){throw b;};d.load=function(b,c,i){d.resourcesReady(!1);b.scriptCount+=1;d.attach(i,b,c);if(b.jQuery&&!b.jQueryIncremented)V(b.jQuery,!0),b.jQueryIncremented=!0};define=function(b,c,d){var j,l;typeof b!=="string"&&(d=c,c=b,b=null);E(c)||(d=c,c=[]);!c.length&&J(d)&&d.length&&(d.toString().replace(ja,"").replace(ka,function(b,d){c.push(d)}),c=(d.length===1?["require"]:["require","exports","module"]).concat(c));if(O&&(j=I||ia()))b||(b=j.getAttribute("data-requiremodule")), 27 | l=u[j.getAttribute("data-requirecontext")];(l?l.defQueue:U).push([b,c,d])};define.amd={multiversion:!0,plugins:!0,jQuery:!0};d.exec=function(b){return eval(b)};d.execCb=function(b,c,d,j){return c.apply(j,d)};d.addScriptToDom=function(b){I=b;z?y.insertBefore(b,z):y.appendChild(b);I=null};d.onScriptLoad=function(b){var c=b.currentTarget||b.srcElement,i;if(b.type==="load"||c&&la.test(c.readyState))m=null,b=c.getAttribute("data-requirecontext"),i=c.getAttribute("data-requiremodule"),u[b].completeLoad(i), 28 | c.detachEvent&&!ca?c.detachEvent("onreadystatechange",d.onScriptLoad):c.removeEventListener("load",d.onScriptLoad,!1)};d.attach=function(b,c,i,j,l,m){var o;if(G)return j=j||d.onScriptLoad,o=c&&c.config&&c.config.xhtml?document.createElementNS("http://www.w3.org/1999/xhtml","html:script"):document.createElement("script"),o.type=l||"text/javascript",o.charset="utf-8",o.async=!w.skipAsync[b],c&&o.setAttribute("data-requirecontext",c.contextName),o.setAttribute("data-requiremodule",i),o.attachEvent&& 29 | !ca?(O=!0,m?o.onreadystatechange=function(){if(o.readyState==="loaded")o.onreadystatechange=null,o.attachEvent("onreadystatechange",j),m(o)}:o.attachEvent("onreadystatechange",j)):o.addEventListener("load",j,!1),o.src=b,m||d.addScriptToDom(o),o;else ba&&(importScripts(b),c.completeLoad(i));return null};if(G){v=document.getElementsByTagName("script");for(H=v.length-1;H>-1&&(A=v[H]);H--){if(!y)y=A.parentNode;if(B=A.getAttribute("data-main")){if(!r.baseUrl)v=B.split("/"),A=v.pop(),v=v.length?v.join("/")+ 30 | "/":"./",r.baseUrl=v,B=A.replace(aa,"");r.deps=r.deps?r.deps.concat(B):[B];break}}}d.checkReadyState=function(){var b=w.contexts,c;for(c in b)if(!(c in K)&&b[c].waitCount)return;d.resourcesReady(!0)};d.resourcesReady=function(b){var c,i;d.resourcesDone=b;if(d.resourcesDone)for(i in b=w.contexts,b)if(!(i in K)&&(c=b[i],c.jQueryIncremented))V(c.jQuery,!1),c.jQueryIncremented=!1};d.pageLoaded=function(){if(document.readyState!=="complete")document.readyState="complete"};if(G&&document.addEventListener&& 31 | !document.readyState)document.readyState="loading",window.addEventListener("load",d.pageLoaded,!1);d(r);if(d.isAsync&&typeof setTimeout!=="undefined")C=w.contexts[r.context||"_"],C.requireWait=!0,setTimeout(function(){C.requireWait=!1;C.takeGlobalQueue();C.jQueryCheck();C.scriptCount||C.resume();d.checkReadyState()},0)}})(); 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | THIS SOFTWARE IS NOT INTENDED FOR PRIMARY DIAGNOSTIC, ONLY FOR SCIENTIFIC USAGE. 2 | 3 | THIS SOFTWARE IS NOT CERTIFIED AS A MEDICAL DEVICE FOR PRIMARY DIAGNOSIS. THERE ARE NO CERTIFICATIONS. YOU CAN ONLY USE THIS SOFTWARE AS A REVIEWING AND SCIENTIFIC SOFTWARE, NOT FOR PRIMARY DIAGNOSTIC. 4 | 5 | ALL CALCULATIONS, MEASUREMENTS AND IMAGES PROVIDED BY THIS SOFTWARE ARE INTENDED ONLY FOR SCIENTIFIC RESEARCH, NOT FOR DIAGNOSIS. 6 | 7 | THE IMAGES DISPLAYED BY THIS VIEWER WHEN USED IN CONJUNCTION WITH THE DCM4CHEE PACS SERVER ARE LOSSY JPEGS AND ARE NOT OF THE SAME QUALITY AS DISPLAYED BY A MEDICAL PACS VIEWER. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, 10 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 11 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 12 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 13 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 14 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE 15 | USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 16 | THIS SOFTWARE IS NOT INTENDED FOR PRIMARY DIAGNOSTIC, ONLY FOR SCIENTIFIC USAGE. 17 | 18 | # StudyCentric - Research DICOM Viewer 19 | 20 | ## What is StudyCentric? 21 | 22 | StudyCentric is web-based DICOM image viewer for use with research applications. The application communicates with a DICOM PACS via the DICOM protocol (both standard DICOM C-FIND queries and via [WADO](http://medical.nema.org/dicom/2004/04_18PU.PDF)). A REST service implemented in Django speaks DICOM to your PACS and exposes necessary queries to the JavaScript client. The rest of application runs entirely in the web browser using JavaScript/HTML. 23 | 24 | StudyCentric is not a full PACS viewer, it is meant to be deployed within a larger application to view specific DICOM studies. It contains no patient or image search functionality. The expected workflow would be that an external system provides links to StudyCentric that specify the Study UID it should display, like below: 25 | 26 | `http://yourinstitution.edu/studycentric/index.html?studyUID=1.2.323.32.3.356` 27 | 28 | ## Feedback 29 | All types of feedback are welcome. Please see the [google user group](https://groups.google.com/forum/#!forum/studycentric). 30 | 31 | ## Screenshot 32 | 33 |
34 | 35 |
36 | 37 | Displaying the KNIX study from http://www.osirix-viewer.com/datasets/ 38 | 39 | 40 | ## Limitations 41 | 42 | StudyCentric currently only supports single frame DICOM files. There is not multi-frame support at this time. It has been tested with the following modality types: 43 | 44 | * MRI 45 | * CT 46 | * X-ray 47 | 48 | ### Notes: 49 | There used to be server implementations in both Ruby and Python. Support for Ruby has been dropped in favor of making the whole app available as a Docker image. 50 | 51 | 52 | You can require that users authenticate before using the application. This uses Django's auth functionality. See [requiring authenticaton](#requiring-authentication) below. 53 | 54 | ## Your PACS 55 | This is a bit beyond the scope of this README but you will need to store your images in a DICOM compatible [PACS](http://en.wikipedia.org/wiki/Picture_archiving_and_communication_system) that supports [WADO](http://medical.nema.org/dicom/2004/04_18PU.PDF). All of our internal instances use the open source [DCM4CHEE](http://www.dcm4che.org/confluence/display/ee2/Home) PACS. 56 | 57 | Both the Server and the Client will need to communicate with the PACS system. The server executes all communication that requires speaking the standard DICOM protocol or manipulating binary files, but the client executes WADO requests (HTTP requests) for images directly. There are three basic ways you can choose to make the PACS WADO port available to the client(by default on DCM4CHEE it is port 8080): 58 | 59 | 1. Make the WADO port directly available to the StudyCentric client. Please note, the WADO protocol will serve up both jpg images (which is what the StudyCentric client requires) and the DICOM files themselves (this functionality is only used by the StudyCentric server, not the client). If you choose this option, all users will technically be able to request the entire DICOM file giving them access to all metadata about the study in the DICOM file (the level of information in the file will depend on your anonymization process) as opposed to simply being able to see the information that the client displays (the images, the pixel spacing data, the study description and the series descriptions). This may or not be what you want. 60 | 1. Make the WADO port of your DICOM server available to the client via a reverse proxy. This would allow you to restrict the types of requests you allow a user to make of the WADO server, for example, dropping all WADO requests for a contenttype of WADO and only allowing JPEG requests through. 61 | 1. Proxy through the StudyCentric server. The StudyCentric client has an endpoint that will proxy requests through to the DICOM WADO server, attempting to drop any requests for the DICOM files (use t 62 | his at your own risk). You can use this by configuring the client's WADO server setting (see below) to point to the /wado endpoint of the StudyCentric server. This method has not been tested for security or performance and will likely be slower than the above two options. 63 | 64 | ## Running with Docker 65 | 66 | The easiest way to get the app up and running is to use Docker. This README will cover deployment with docker only. If you want to deploy the app yourself, there are instructions in this repo's wiki. 67 | ### Get the docker image 68 | 69 | There is a pre-built docker image in the docker hub, just run: 70 | 71 | docker pull dbhi/studycentric 72 | 73 | If you prefer to build the image yourself, see the next section. 74 | 75 | ### Building the docker image 76 | 77 | Clone this repository and run the following command: 78 | 79 | docker build -t studycentric . 80 | 81 | ### Configuration 82 | 83 | The following environment variables will allow you to configure StudyCentric. While most variables have defaults, you will probably need to at least set `DICOM_SERVER`. Below is a list of the variables and what they do: 84 | 85 | DICOM_SERVER: This is the ip address of your PACS. Defaults to "localhost". 86 | DICOM_PORT: This is the DICOM port of your PACS. Defaults to 11112. 87 | WADO_SERVER: This is the server exposing the WADO service that StudyCentric will talk to. Defaults to the value set for `DICOM_SERVER`. 88 | WADO_PORT: This is the port for the WADO service that your StudyCentric will talk to. Defaults to 8080. 89 | WADO_PATH: This the path in URI to the WADO service that your StudyCentric will speak to. Defaults to 'wado'. 90 | WADO_PROT: This is the protocol your WADO service is running on (http or https). Defaults to http. 91 | DICOM_AET: The AET of your PACS. Defaults to DCM4CHEE. 92 | 93 | LOGIN_ENABLED: Enable Django authenticatoin. See the [Requiring Authentication](#requiring-authentication) section below. Defaults to 0 for false. 94 | DJANGO_DB_NAME: Set the name of the sqlite database Django will use if you set LOGIN_ENABLED to 1. Defaults to "database.db". 95 | DJANGO_ADMIN_USER: Set Django admin superuser name. 96 | DJANGO_ADMIN_EMAIL: Set Django admin superuser email. 97 | DJANGO_ADMIN_PASSWORD: Set Django admin superuser password. 98 | 99 | INSTANCE_THUMBNAIL_SIZE_PX: This controls the size of the image thumbnails that appear in the right-hand side series preview drawer. Defaults to 100. 100 | SERIES_THUMBNAIL_SIZE_PX: This controls the thumbnail size for the Series thumbnails displayed on the left-hand side of the screen. Defaults to 150. 101 | DEFAULT_IMG_SIZE: This controls the default image size of the displayed image in the center of the screen. Defaults to 128. 102 | IMAGES_PER_ROW: This controls the number of images displayed per row in the right-hand side series preview drawer. Defaults to 3 103 | DISABLE_CLINICAL_WARNING: This controls whether StudyCentric will prompt the user to agree that it is not to be used for clinical or diagnostic purposes. The default is false, so the user will be prompted each time they use the application. It is recommended that this default be used. Defaults to False. 104 | MEASUREMENT_PRECISION: This determines the number of decimal places shown in in the distance measurements (in mm or pixels). It is very important to keep in mind that the measurement tool is not high quality and is not meant to be used for diagnosis. The appearance of images can change across different browsers because StudyCentric requests lossy jpegs from the PACS and interpolation algorithms vary. Care should be taken to not provide a false sense of accuracy by providing more precision than the data and image actually provides. Defaults to 1. 105 | JSONP: Enable JSONP for ajax requests. Defaults to 1 for true. 106 | HOVER_COLOR: You can change the default color when you hover over measurements. Defaults to #FFAA56. 107 | HINT_COLOR: Change the default color of hint lines between measurement bars measurement text. Defaults to #FFFFFF. 108 | MEASUREMENT_COLOR: Change the default color of measurements. Defaults to #00FF00. 109 | PNG: Request lossless pngs from your WADO server instead of JPEGs. Defaults to false. 110 | 111 | The following variables are available in the event that you need to force your client to make WADO requests (which is how it obtains the actual images it displays) to a different PACS then specified above. This would only happen in a situation where the network you are serving StudyCentric to does not have direct access to your PACS and it needs to go through some sort of proxy. See the [Your PACS](#your-pacs) section for more details. If your client can talk directly to your PACS you can ignore these variables completely. 112 | 113 | CLIENT_WADO_HOST: WADO host you want your client to talk to. 114 | CLIENT_WADO_PORT: Port the WADO service is running on 115 | CLIENT_WADO_PROT: Protocol the WADO service is using (http or https) 116 | CLIENT_WADO_PATH: Path to the WADO service on 117 | 118 | ##### Report a Concern Feature 119 | 120 | ENABLE_REPORT_CONCERN 121 | REPORT_CONCERN_URL 122 | REPORT_CONCERN_EMAIL 123 | 124 | The last three options affect an optional "Report a Concern" feature. This is meant to help screen studies for potential Protected Health Information(PHI). If `ENABLE_REPORT_CONCERN` is set to 1, this places a button at the top of the screen labeled "Report a Concern". If the user clicks the button while viewing a study it will POST a JSON object to the `REPORT_CONCERN_URL` to inform you that something in the study requires attention. *Please note that the HTTP service receiving this POST is not part of StudyCentric, you would need to provide it.* The feature is intended for use when StudyCentric is part of a larger PHI screening application. The `REPORT_CONCERN_EMAIL ` is displayed to the user in a message asking them to e-mail the study information only if the POST fails. 125 | 126 | The JSON object sent is as follows: 127 | 128 | ``` javascript 129 | { 130 | document: , 131 | comment: 132 | } 133 | ``` 134 | 135 | ### Running 136 | Once the configuration is set, you can run the image as follows. This assumes that you have put all of the necessary variables above into a docker environment variable file called `env_vars` and want to expose the service on port 8000 of your docker host. 137 | 138 | docker run -p 8000:8000 --env-file=env_vars studycentric 139 | 140 | The server should be available at `http://yourdockerhost:8000/static/index.html?studyUID=` unless you turned on `LOGIN_ENABLED` in which case it will be `http://yourdockerhost:8000/app/?studyUID=`. Read the next section for more details on this feature. 141 | 142 | #### Requiring Authentication 143 | The original intention of StudyCentric was to make it a simple JavaScript/HTML only app that hits a simple web service only when absolutely required for browser limitations or performance reasons. If that is all you need, you can still do this. Simply run the service as is. Realistically, because of the nature of this type of application, you may have authentication requirements. You can accomplish this at the webserver level with something like http basic auth, but for a better user experience the server can be configured to require authentication. If the `LOGIN_ENABLED` environment variable is set to 1, it will essentially turn the app into a Django app that requires the user to authenticate. Basically, instead of pointing users to the StudyCentric index.html static file, you point them to the url endpoint `app/` that will require authentication before showing anything. 144 | 145 | ##### Enabling authentication 146 | As this turns the project into a more complex django application, this may require some knowledge of django, but this guide will try to walk through all the steps. 147 | 148 | 1. Set the `LOGIN_ENABLED` environment variable to 1. 149 | 1. You need a Django database backend to hold the authentication and session information (this is required by Django when you use its authentication features). By default, StudyCentric will create a sqlite database (the location and name of which you can control with the `DJANGO_DB_NAME` environment variable), but you can change this by using docker to mount a local_settings.py file into /opt/app/server/ with the proper Django server settings. You can also use this method to add any middleware or custom authentication backends you might need. The login screen is pretty barebones- you can override it by mounting a custom template to `/opt/server/templates/registration/login.html`. If you want to create a superuser for your Django database, you must set the `DJANGO_ADMIN_USER`, `DJANGO_ADMIN_EMAIL` and (optionally, depending on whether you have a custom authentication backend) `DJANGO_ADMIN_PASSWORD` environment variables. If you do not, no super user will be created, and if you have not set up a custom authentication backend you will not be able to login. 150 | 151 | # Troubleshooting 152 | 153 | * The page loads, but it is completely blank (no series images load along the left hand side) 154 | 155 | Verify that your `DICOM_SERVER` and associated environment variables are set correctly. 156 | 157 | * The page loads fine, but when you choose a series from the left, the first image does not appear in the center pane 158 | Try changing `JSONP` environment variable to 1. The is likely a CORS (Cross Origin Resource Sharing) issue, and this 159 | should fix it. 160 | 161 | 162 | -------------------------------------------------------------------------------- /client/js/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | paths: { 3 | 'jquery': 'libs/jquery', 4 | 'underscore': 'libs/underscore-min', 5 | 'backbone': 'libs/backbone' // AMD support 6 | } 7 | }); 8 | require(['jquery', 9 | 'models', 10 | 'config', 11 | 'utils', 12 | 'lightbox', 13 | 'underscore', 14 | 'reportaconcern', 15 | 'libs/jquery.mousewheel', 16 | 'libs/jquery-ui-min', 17 | 'libs/jquery.nanoscroller.min', 18 | "libs/raphael-min" 19 | ], function($, Models, Config, Utils, LightBox, _) { 20 | 21 | var lightBox; 22 | var t_window = _.template("

Window Center: <%=center%>

Window Width: <%=width%>

"); 23 | var handleWindow = function(){}; 24 | 25 | $("#instancepanel.nano").nanoScroller(); 26 | $("#series.nano").nanoScroller(); 27 | 28 | $("#lightbox").addClass("interpol").hide(); 29 | function changeZoom(event, ui){ 30 | // If we don't have the data for this image yet, we can't zoom 31 | if (lightBox.data('dicom_attrs') === null) return; 32 | // How this works: 33 | // If the image's current size is smaller than its native size 34 | // we ask the server to do the zoom for us, either wise we just 35 | // make it bigger on the browser since we already have all the data 36 | var dicom_attrs = lightBox.data('dicom_attrs') 37 | var nativeRows = dicom_attrs["nativeRows"]; 38 | var screenRows = lightBox.prop("height"); 39 | if (ui.value === 0){ 40 | newRows = nativeRows; 41 | }else { 42 | newRows = nativeRows + ui.value;//ath.floor(nativeRows * (1 - Math.abs(ui.value)/(3*nativeRows))); 43 | } 44 | if (ui.value >=0 && newRows > screenRows && newRows <= nativeRows){ 45 | // change img src to get the servers new image 46 | lightBox.prop("height", newRows); // Change in case its no longer not set 47 | var qsObject = Utils.queryString2Object(lightBox[0].src); 48 | qsObject.rows = newRows; 49 | lightBox[0].src = lightBox[0].src.split("?")[0]+"?"+$.param(qsObject).replace("252F", "2F"); 50 | }else{ 51 | lightBox.prop("height", newRows); 52 | } 53 | // Keep image centered as it gets larger or smaller 54 | var diff = newRows - screenRows; 55 | lightBox.css("top", parseInt(lightBox.css("top")) - diff/2+"px"); 56 | lightBox.css("left", parseInt(lightBox.css("left")) - diff/2+"px"); 57 | } 58 | 59 | // Set the slider to the right spot 60 | function setZoomScale(whenDone){ 61 | if (lightBox.data('dicom_attrs') === null) { 62 | $("#slider").slider("disable"); 63 | // Try again 64 | setTimeout(function(){setZoomScale(whenDone);}, 500); 65 | return; 66 | } 67 | $("#slider").slider("destroy"); 68 | var nativeRows = lightBox.data('dicom_attrs')["nativeRows"]; 69 | $("#slider").slider({ 70 | orientation:"vertical", 71 | range:"min", 72 | min: 0-nativeRows, 73 | max: 5*nativeRows, 74 | step: 2, 75 | slide: changeZoom 76 | }); 77 | $("#slider").slider("enable"); 78 | changeZoom(undefined, {value:0}); 79 | whenDone && whenDone(); 80 | } 81 | 82 | function centerLightBox(){ 83 | if (!lightBox.width()){ 84 | setTimeout(centerLightBox, 100); 85 | return; 86 | } 87 | var start = $("#series").width() + parseInt($("#series").css("padding-left")); 88 | var end = $("#instancepanel").width(); 89 | var center_width = $(window).width() - start - end; 90 | var spot = center_width/2 - lightBox.width()/2; 91 | lightBox.css("position","absolute"); 92 | lightBox.css("left", spot+"px"); 93 | lightBox.css("top", $("#center").height()/2 - lightBox.height()/2 - 50); 94 | lightBox.show(); 95 | } 96 | 97 | $(function() { 98 | $("body").bind("null_dicom_attr", function(){ 99 | $('#measure').addClass("disabled"); 100 | }); 101 | $("body").bind("dicom_attr_received", function(){ 102 | $('#measure').removeClass("disabled"); 103 | // handleWindow(); 104 | setWWWL(0,0); 105 | }); 106 | $("body").bind("study/new", function(){ 107 | setZoomScale(function(){ 108 | centerLightBox(); 109 | }); 110 | }); 111 | 112 | function setWWWL(deltaWW, deltaWL){ 113 | var current_src = lightBox[0].src; 114 | var current_attrs = lightBox.data("dicom_attrs"); 115 | var obj = Utils.queryString2Object(current_src); 116 | 117 | // We don't yet have the windowWidth, try again when the next ajax request finishes 118 | //if (!current_attrs && !obj.windowCenter) { 119 | // handleWindow=function(){setWWWL(deltaWW,deltaWW);}; 120 | // return; 121 | //} 122 | // handleWindow = function(){}; 123 | obj.windowCenter = obj.windowCenter ? parseInt(obj.windowCenter)+deltaWL : current_attrs.windowCenter + deltaWL; 124 | obj.windowWidth = obj.windowWidth ? parseInt(obj.windowWidth)+deltaWW : current_attrs.windowWidth + deltaWW; 125 | obj.windowCenter = obj.windowCenter < 1 ? 1 : obj.windowCenter; 126 | obj.windowWidth = obj.windowWidth < 1 ? 1 : obj.windowWidth; 127 | 128 | $("#window_info").html(t_window({center: obj.windowCenter, width: obj.windowWidth})); 129 | 130 | // Don't waste time setting these attributes if they are the defaults, it will cause unnecessary server hits 131 | // but if we set them because none were provided by the server, leave them. 132 | if (!current_attrs.defaults && (obj.windowWidth === parseInt(current_attrs.windowWidth)) && 133 | (obj.windowCenter === parseInt(current_attrs.windowCenter))){ 134 | delete obj["windowWidth"]; 135 | delete obj["windowCenter"]; 136 | } 137 | 138 | // If we actually changed something, change the image. 139 | if (deltaWW && deltaWL) lightBox[0].src = [lightBox[0].src.split('?')[0], jQuery.param(obj).replace("252F", "2F")].join("?"); 140 | }; 141 | 142 | $("#handle").toggle(function(){ 143 | $("#instancepanel").css("width","320px"); 144 | $("#handle").css("right","323px"); 145 | $("#arrow").addClass("right").removeClass("left"); 146 | }, 147 | function(){ 148 | $("#instancepanel").css("width","0px"); 149 | $("#handle").css("right","3px"); $("#arrow").addClass("left").removeClass("right"); 150 | }); 151 | 152 | var lb; 153 | var disable = function(){}; 154 | 155 | // This function handles animating and scrolling via the scroll 156 | // wheel 157 | var may_continue = true; 158 | var animateSeries = function(event, delta, loop){ 159 | // This code puts a throttle on how fast you can zip through images 160 | // because on faster browsers the nice movie effect wasn't working for 161 | // smaller images 162 | if (!may_continue){ 163 | return; 164 | } 165 | var t = setTimeout(function(){ 166 | lightBox.unbind("load"); 167 | may_continue = true; 168 | }, 3000); 169 | lightBox.one("load", function(){ 170 | may_continue = true; 171 | clearTimeout(t); 172 | }); 173 | 174 | may_continue = false; 175 | var dir = delta > 0 ? 'Up' : 'Down'; 176 | var m = lightBox.data("model"); 177 | var i = lightBox.data("index"); 178 | var length = m.collection.models.length; 179 | var next_m; 180 | if (dir === "Up" && i > 0) { 181 | next_m = m.collection.models[i-1]; 182 | lightBox.data("model", next_m); 183 | lightBox.data("index", $.inArray(next_m, m.collection.models)); 184 | next_m.trigger("scroll_show"); 185 | } else if (dir === "Down" && i < length-1){ 186 | next_m = m.collection.models[i+1]; 187 | lightBox.data("model", next_m); 188 | lightBox.data("index", $.inArray(next_m, m.collection.models)); 189 | next_m.trigger("scroll_show"); 190 | } else if (loop) { 191 | next_m = m.collection.models[0]; 192 | lightBox.data("model", next_m); 193 | lightBox.data("index", 0); 194 | next_m.trigger("scroll_show"); 195 | } 196 | if (!loop &&((dir === "Down" && i === length-1) || (dir === "Up" && i === 0))){ 197 | may_continue = true; 198 | clearTimeout(t); 199 | lightBox.unbind("load"); 200 | } 201 | }; 202 | 203 | // Bind arrow keys to run through the series 204 | $(document).keydown(function(event){ 205 | if (event.which in {39:1, 40:1}){ 206 | animateSeries(undefined, 0); 207 | return false; 208 | } else if (event.which in {37:1, 38:1}){ 209 | animateSeries(undefined, 1); 210 | return false; 211 | } 212 | }); 213 | 214 | $("#slider").slider({ 215 | orientation:"vertical", 216 | range:"min", 217 | value:10, 218 | min: 10, 219 | max: 250, 220 | step: 10, 221 | slide: changeZoom 222 | }); 223 | 224 | $("#move").click(function(){ 225 | if ($(this).hasClass("selected")) return false; 226 | $(this).siblings().removeClass("selected"); 227 | $(this).addClass("selected"); 228 | disable(); 229 | lightBox.draggable({scroll:false}); 230 | disable = function(){ 231 | lightBox.draggable("destroy"); 232 | }; 233 | return false; 234 | }); 235 | 236 | $("#recover").click(function(){ 237 | if ($(this).hasClass("disabled")) return false; 238 | if (!$("#lightbox").is(":visible")) return false; 239 | // We can't move when measure is on. 240 | if ($("#measure").hasClass("selecte")) return false; 241 | centerLightBox(); 242 | }); 243 | 244 | $("#measure").click(function(){ 245 | if (!$("#lightbox").is(":visible")) return false; 246 | if ($(this).hasClass("disabled")) return false; 247 | if ($(this).hasClass("selected")) { 248 | lb.clear(); 249 | return false; 250 | } 251 | $(this).siblings().removeClass("selected"); 252 | $(this).addClass("selected"); 253 | disable(); 254 | lb = LightBox("lightbox"); 255 | lb.measureOn(); 256 | disable = function(){ 257 | lb.measureOff(); lb.destroy(); 258 | $("#slider").slider("enable"); 259 | }; 260 | $("#slider").slider("disable"); 261 | return false; 262 | }); 263 | 264 | $("#animate").click(function(){ 265 | if (!$("#lightbox").is(":visible")) return false; 266 | // This button is a toggle 267 | if ($(this).hasClass("selected")) { 268 | $(this).removeClass("selected"); 269 | disable(); 270 | disable = function(){}; 271 | return false; 272 | } 273 | $(this).siblings().removeClass("selected"); 274 | $(this).addClass("selected"); 275 | disable(); 276 | var interval = setInterval(function(){ 277 | animateSeries(null,-1, true); 278 | }, 100); 279 | disable = function(){ 280 | clearInterval(interval); 281 | $("#slider").slider("enable"); 282 | }; 283 | $("#slider").slider("disable"); 284 | return false; 285 | }); 286 | 287 | // Window level functionality 288 | $("#window").click(function(){ 289 | if (!$("#lightbox").is(":visible")) return false; 290 | if ($(this).hasClass("selected")) return false; 291 | $(this).siblings().removeClass("selected"); 292 | $(this).addClass("selected"); 293 | disable(); 294 | var start = undefined; 295 | var attrs = lightBox.data("dicom_attrs"); 296 | lightBox.mousedown(function(event){ 297 | start = { x: event.pageX, 298 | y: event.pageY }; 299 | // This is to fix an IE bug where mouseup doesn't work on images after a drag 300 | lightBox.bind("mousemove.wl", function(){return false;}); 301 | $(document).one("mouseup", function(event){ 302 | lightBox.unbind(".wl"); 303 | var deltaWL = event.pageY - start.y;//windowCenter 304 | var deltaWW = event.pageX - start.x;//windowWidth 305 | setWWWL(deltaWL, deltaWW); 306 | }); 307 | return false; 308 | }); 309 | 310 | disable = function(){ 311 | lightBox.unbind("mousedown"); 312 | lightBox.unbind("mouseup"); 313 | }; 314 | }); 315 | 316 | 317 | var config = Utils.queryString2Object(window.location.search); 318 | window.study = new Models.Study({uid:config.uid || 319 | config.study_uid || 320 | config.studyuid || 321 | config.studyUID}); 322 | if (config.instances) $("#handle").click(); 323 | lightBox = $("#lightbox"); 324 | lightBox.mousewheel(animateSeries); 325 | $("#slider").slider("disable"); 326 | 327 | if (!Config.DisableClinicalWarning){ 328 | if ($().dialog){ 329 | $( "#dialog-confirm" ).dialog({ 330 | resizable: false, 331 | draggable: false, 332 | height:200, 333 | width:350, 334 | modal: true, 335 | closeOnEscape: false, 336 | buttons: { 337 | "I agree": function() { 338 | $(this).dialog( "close" ); 339 | } 340 | } 341 | }); 342 | }else{ 343 | alert("StudyCentric is not a certified Medical Device and is not to be used for any clinical or diagnostic purposes. It is for research only."); 344 | } 345 | } 346 | 347 | }); 348 | }); 349 | -------------------------------------------------------------------------------- /client/css/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * HTML5 ✰ Boilerplate 3 | * 4 | * style.css contains a reset, font normalization and some base styles. 5 | * 6 | * Credit is left where credit is due. 7 | * Much inspiration was taken from these projects: 8 | * - yui.yahooapis.com/2.8.1/build/base/base.css 9 | * - camendesign.com/design/ 10 | * - praegnanz.de/weblog/htmlcssjs-kickstart 11 | */ 12 | 13 | 14 | /** 15 | * html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline) 16 | * v1.6.1 2010-09-17 | Authors: Eric Meyer & Richard Clark 17 | * html5doctor.com/html-5-reset-stylesheet/ 18 | */ 19 | 20 | html, body, div, span, object, iframe, 21 | h1, h2, h3, h4, h5, h6, p, blockquote, pre, 22 | abbr, address, cite, code, del, dfn, em, img, ins, kbd, q, samp, 23 | small, strong, sub, sup, var, b, i, dl, dt, dd, ol, ul, li, 24 | fieldset, form, label, legend, 25 | table, caption, tbody, tfoot, thead, tr, th, td, 26 | article, aside, canvas, details, figcaption, figure, 27 | footer, header, hgroup, menu, nav, section, summary, 28 | time, mark, audio, video { 29 | margin: 0; 30 | padding: 0; 31 | border: 0; 32 | font-size: 100%; 33 | font: inherit; 34 | vertical-align: baseline; 35 | } 36 | 37 | article, aside, details, figcaption, figure, 38 | footer, header, hgroup, menu, nav, section { 39 | display: block; 40 | } 41 | 42 | .ui-dialog-titlebar-close { 43 | visibility: hidden; 44 | } 45 | 46 | blockquote, q { quotes: none; } 47 | 48 | blockquote:before, blockquote:after, 49 | q:before, q:after { content: ""; content: none; } 50 | 51 | ins { background-color: #ff9; color: #000; text-decoration: none; } 52 | 53 | mark { background-color: #ff9; color: #000; font-style: italic; font-weight: bold; } 54 | 55 | del { text-decoration: line-through; } 56 | 57 | abbr[title], dfn[title] { border-bottom: 1px dotted; cursor: help; } 58 | 59 | table { border-collapse: collapse; border-spacing: 0; } 60 | 61 | hr { display: block; height: 1px; border: 0; border-top: 1px solid #ccc; margin: 1em 0; padding: 0; } 62 | 63 | input, select { vertical-align: middle; } 64 | 65 | 66 | /** 67 | * Font normalization inspired by YUI Library's fonts.css: developer.yahoo.com/yui/ 68 | */ 69 | 70 | body { font:13px/1.231 sans-serif; *font-size:small; } /* Hack retained to preserve specificity */ 71 | select, input, textarea, button { font:99% sans-serif; } 72 | 73 | /* Normalize monospace sizing: 74 | en.wikipedia.org/wiki/MediaWiki_talk:Common.css/Archive_11#Teletype_style_fix_for_Chrome */ 75 | pre, code, kbd, samp { font-family: monospace, sans-serif; } 76 | 77 | 78 | /** 79 | * Minimal base styles. 80 | */ 81 | 82 | /* Always force a scrollbar in non-IE */ 83 | html { overflow-y: scroll; } 84 | 85 | /* Accessible focus treatment: people.opera.com/patrickl/experiments/keyboard/test */ 86 | a:hover, a:active { outline: none; } 87 | 88 | ul, ol { margin-left: 2em; } 89 | ol { list-style-type: decimal; } 90 | 91 | /* Remove margins for navigation lists */ 92 | nav ul, nav li { margin: 0; list-style:none; list-style-image: none; } 93 | 94 | small { font-size: 85%; } 95 | strong, th { font-weight: bold; } 96 | 97 | td { vertical-align: top; } 98 | 99 | /* Set sub, sup without affecting line-height: gist.github.com/413930 */ 100 | sub, sup { font-size: 75%; line-height: 0; position: relative; } 101 | sup { top: -0.5em; } 102 | sub { bottom: -0.25em; } 103 | 104 | pre { 105 | /* www.pathf.com/blogs/2008/05/formatting-quoted-code-in-blog-posts-css21-white-space-pre-wrap/ */ 106 | white-space: pre; white-space: pre-wrap; word-wrap: break-word; 107 | padding: 15px; 108 | } 109 | 110 | textarea { overflow: auto; } /* www.sitepoint.com/blogs/2010/08/20/ie-remove-textarea-scrollbars/ */ 111 | 112 | .ie6 legend, .ie7 legend { margin-left: -7px; } 113 | 114 | /* Align checkboxes, radios, text inputs with their label by: Thierry Koblentz tjkdesign.com/ez-css/css/base.css */ 115 | input[type="radio"] { vertical-align: text-bottom; } 116 | input[type="checkbox"] { vertical-align: bottom; } 117 | .ie7 input[type="checkbox"] { vertical-align: baseline; } 118 | .ie6 input { vertical-align: text-bottom; } 119 | 120 | /* Hand cursor on clickable input elements */ 121 | label, input[type="button"], input[type="submit"], input[type="image"], button { cursor: pointer; } 122 | 123 | /* Webkit browsers add a 2px margin outside the chrome of form elements */ 124 | button, input, select, textarea { margin: 0; } 125 | 126 | /* Colors for form validity */ 127 | input:valid, textarea:valid { } 128 | input:invalid, textarea:invalid { 129 | border-radius: 1px; -moz-box-shadow: 0px 0px 5px red; -webkit-box-shadow: 0px 0px 5px red; box-shadow: 0px 0px 5px red; 130 | } 131 | .no-boxshadow input:invalid, .no-boxshadow textarea:invalid { background-color: #f0dddd; } 132 | 133 | 134 | /* These selection declarations have to be separate 135 | No text-shadow: twitter.com/miketaylr/status/12228805301 136 | Also: hot pink! */ 137 | ::-moz-selection{ background:#000000; color:#fff; text-shadow: none; } 138 | ::selection { background:#000000; color:#fff; text-shadow: none; } 139 | 140 | /* j.mp/webkit-tap-highlight-color */ 141 | a:link { -webkit-tap-highlight-color: #FF5E99; } 142 | 143 | /* Make buttons play nice in IE: 144 | www.viget.com/inspire/styling-the-button-element-in-internet-explorer/ */ 145 | button { width: auto; overflow: visible; } 146 | 147 | /* Bicubic resizing for non-native sized IMG: 148 | code.flickr.com/blog/2008/11/12/on-ui-quality-the-little-things-client-side-image-resizing/ */ 149 | .ie7 img { -ms-interpolation-mode: bicubic; } 150 | 151 | /** 152 | * You might tweak these.. 153 | */ 154 | 155 | body, select, input, textarea { 156 | /* #444 looks better than black: twitter.com/H_FJ/statuses/11800719859 */ 157 | color: #444; 158 | /* Set your base font here, to apply evenly */ 159 | /* font-family: Georgia, serif; */ 160 | } 161 | 162 | /* Headers (h1, h2, etc) have no default font-size or margin; define those yourself */ 163 | h1, h2, h3, h4, h5, h6 { font-weight: bold; } 164 | 165 | a, a:active, a:visited { color: #607890; } 166 | a:hover { color: #036; } 167 | 168 | 169 | /** 170 | * Primary styles 171 | * 172 | * Author: 173 | */ 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | /** 191 | * Non-semantic helper classes: please define your styles before this section. 192 | */ 193 | 194 | /* For image replacement */ 195 | .ir { display: block; text-indent: -999em; overflow: hidden; background-repeat: no-repeat; text-align: left; direction: ltr; } 196 | 197 | /* Hide for both screenreaders and browsers: 198 | css-discuss.incutio.com/wiki/Screenreader_Visibility */ 199 | .hidden { display: none; visibility: hidden; } 200 | 201 | /* Hide only visually, but have it available for screenreaders: by Jon Neal. 202 | www.webaim.org/techniques/css/invisiblecontent/ & j.mp/visuallyhidden */ 203 | .visuallyhidden { border: 0; clip: rect(0 0 0 0); height: 1px; margin: -1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } 204 | /* Extends the .visuallyhidden class to allow the element to be focusable when navigated to via the keyboard: drupal.org/node/897638 */ 205 | .visuallyhidden.focusable:active, 206 | .visuallyhidden.focusable:focus { clip: auto; height: auto; margin: 0; overflow: visible; position: static; width: auto; } 207 | 208 | /* Hide visually and from screenreaders, but maintain layout */ 209 | .invisible { visibility: hidden; } 210 | 211 | /* The Magnificent Clearfix: Updated to prevent margin-collapsing on child elements. 212 | j.mp/bestclearfix */ 213 | .clearfix:before, .clearfix:after { content: "\0020"; display: block; height: 0; overflow: hidden; } 214 | .clearfix:after { clear: both; } 215 | /* Fix clearfix: blueprintcss.lighthouseapp.com/projects/15318/tickets/5-extra-margin-padding-bottom-of-page */ 216 | .clearfix { zoom: 1; } 217 | 218 | 219 | 220 | /** 221 | * Media queries for responsive design. 222 | * 223 | * These follow after primary styles so they will successfully override. 224 | */ 225 | 226 | @media all and (orientation:portrait) { 227 | /* Style adjustments for portrait mode goes here */ 228 | 229 | } 230 | 231 | @media all and (orientation:landscape) { 232 | /* Style adjustments for landscape mode goes here */ 233 | 234 | } 235 | 236 | /* Grade-A Mobile Browsers (Opera Mobile, Mobile Safari, Android Chrome) 237 | consider this: www.cloudfour.com/css-media-query-for-mobile-is-fools-gold/ */ 238 | @media screen and (max-device-width: 480px) { 239 | 240 | 241 | /* Uncomment if you don't want iOS and WinMobile to mobile-optimize the text for you: j.mp/textsizeadjust */ 242 | /* html { -webkit-text-size-adjust:none; -ms-text-size-adjust:none; } */ 243 | } 244 | 245 | 246 | /** 247 | * Print styles. 248 | * 249 | * Inlined to avoid required HTTP connection: www.phpied.com/delay-loading-your-print-css/ 250 | */ 251 | @media print { 252 | * { background: transparent !important; color: black !important; text-shadow: none !important; filter:none !important; 253 | -ms-filter: none !important; } /* Black prints faster: sanbeiji.com/archives/953 */ 254 | a, a:visited { color: #444 !important; text-decoration: underline; } 255 | a[href]:after { content: " (" attr(href) ")"; } 256 | abbr[title]:after { content: " (" attr(title) ")"; } 257 | .ir a:after, a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } /* Don't show links for images, or javascript/internal links */ 258 | pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } 259 | thead { display: table-header-group; } /* css-discuss.incutio.com/wiki/Printing_Tables */ 260 | tr, img { page-break-inside: avoid; } 261 | @page { margin: 0.5cm; } 262 | p, h2, h3 { orphans: 3; widows: 3; } 263 | h2, h3{ page-break-after: avoid; } 264 | } 265 | 266 | /*Custom JMM*/ 267 | 268 | #toolbar { 269 | left:15px; 270 | position:absolute; 271 | top:10px; 272 | } 273 | 274 | #center { 275 | position:fixed; 276 | top:50px; 277 | left:190px; 278 | height:100%; 279 | width:100%; 280 | z-index:0; 281 | } 282 | 283 | #instances { 284 | margin-right:20px; 285 | } 286 | 287 | .selected { 288 | border: 1px solid red; 289 | } 290 | 291 | table img { 292 | padding:0px 0px 0px 0px; 293 | border: 1px solid; 294 | } 295 | 296 | ::-webkit-scrollbar { 297 | width: 10px; 298 | height: 10px; 299 | } 300 | 301 | ::-webkit-scrollbar-track { 302 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); 303 | border-radius: 10px; 304 | } 305 | 306 | ::-webkit-scrollbar-thumb { 307 | border-radius: 10px; 308 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.5); 309 | } 310 | 311 | #slider { 312 | position:absolute; 313 | height:250px; 314 | left:20px; 315 | width:5px; 316 | z-index:1; 317 | } 318 | 319 | ul {list-style-type:none} 320 | 321 | #lightbox { 322 | display:block; 323 | margin-left:auto; 324 | margin-right:auto; 325 | position:absolute; 326 | } 327 | 328 | #header{ 329 | position:fixed; 330 | height:30px; 331 | width:100%; 332 | z-index:6; 333 | } 334 | 335 | #footer { 336 | position:fixed; 337 | height:50px; 338 | bottom:10px; 339 | width:100%; 340 | z-index:6; 341 | } 342 | 343 | input { 344 | margin-left:auto; 345 | margin-right:auto; 346 | text-align:center; 347 | } 348 | 349 | html { 350 | height:100%; 351 | width:100%; 352 | } 353 | 354 | body { 355 | background-color:black; 356 | height:100%; 357 | width:100%; 358 | } 359 | 360 | .crosshair { 361 | cursor:crosshair; 362 | } 363 | 364 | .move { 365 | cursor:pointer; 366 | } 367 | 368 | .ui-draggable { 369 | cursor:move; 370 | } 371 | 372 | #instancepanel { 373 | position:absolute; 374 | width:0px; 375 | border-left:4px solid grey; 376 | padding-left: 2px; 377 | height:90%; 378 | overflow-x:hidden; 379 | overflow-y:auto; 380 | top:50px; 381 | right:0px; 382 | z-index:5; 383 | } 384 | 385 | #handle { 386 | position:absolute; 387 | right:3px; 388 | top:100px; 389 | height:20px; 390 | width:15px; 391 | background:grey; 392 | -moz-border-top-left-radius: 3px; 393 | -moz-border-bottom-left-radius: 3px; 394 | border-top-left-radius: 3px; 395 | border-bottom-left-radius: 3px; 396 | z-index:5; 397 | } 398 | 399 | #series { 400 | border-right: 4px solid grey; 401 | padding-right:2em; 402 | left:0px; 403 | top:50px; 404 | position: fixed; 405 | width: 170px; 406 | height: 90%; 407 | overflow-x:hidden; 408 | overflow-y:auto; 409 | } 410 | .series { 411 | margin-left:20px; 412 | } 413 | 414 | #series img { 415 | margin-bottom: 15px; 416 | } 417 | 418 | p.series_description { 419 | color: white; 420 | display:block; 421 | width: 150px; 422 | text-overflow:ellipsis; 423 | white-space: nowrap; 424 | overflow:hidden; 425 | } 426 | 427 | p.full_series_description { 428 | position:absolute; 429 | display:none; 430 | color:#3388ee; 431 | background-color:white; 432 | width:auto; 433 | border: 1px solid black; 434 | } 435 | 436 | #study_description { 437 | padding-top:10px; 438 | font-size:24px; 439 | color:white; 440 | text-align:center; 441 | } 442 | 443 | #image_number{ 444 | font-size:18px; 445 | color:white; 446 | text-align:center; 447 | } 448 | 449 | .dicom_info { 450 | font-size: 14px; 451 | width:250px; 452 | height:40px; 453 | border:2px solid #3388ee; 454 | color: white; 455 | padding:3px; 456 | position:absolute; 457 | -moz-border-radius: 3px; 458 | border-radius: 3px; 459 | line-height: 20px; 460 | } 461 | 462 | #pixel_spacing{ 463 | left:20px; 464 | } 465 | 466 | #window_info { 467 | right:20px; 468 | } 469 | 470 | li { 471 | display:block; 472 | position:relative; 473 | width:160px; 474 | } 475 | #arrow.left { 476 | margin-top:4px; 477 | margin-left:-6px; 478 | width:0px; 479 | height:0px; 480 | border-color:transparent white transparent transparent; 481 | border-style:solid; 482 | border-width:6px; 483 | } 484 | #arrow.right { 485 | margin-top:4px; 486 | width:0px; 487 | height:0px; 488 | border-color:transparent transparent transparent white; 489 | border-style:solid; 490 | border-width:6px; 491 | } 492 | #pixel_message{ 493 | font-size:18px; 494 | color:red; 495 | text-align:center; 496 | } 497 | .interpol { -ms-interpolation-mode: bicubic; } 498 | 499 | 500 | /*NanoScroller.css*/ 501 | 502 | /** initial setup **/ 503 | .nano { 504 | position : relative; 505 | width : 100%; 506 | height : 100%; 507 | overflow : hidden; 508 | } 509 | .nano .content { 510 | position : absolute; 511 | overflow : scroll; 512 | overflow-x : hidden; 513 | top : 0; 514 | right : 0; 515 | bottom : 0; 516 | left : 0; 517 | } 518 | .nano .pane { 519 | background: rgba(0,0,0,.25); 520 | position : absolute; 521 | width : 10px; 522 | right : 0; 523 | top : 0; 524 | bottom : 0; 525 | } 526 | .nano .pane .slider { 527 | background: rgba(0,0,0,.5); 528 | position : relative; 529 | margin : 0 1px 0 1px; 530 | border-radius : 3px; 531 | } 532 | .nano:hover .pane, .pane.active { 533 | visibility: visible; 534 | } 535 | .nano .pane { 536 | -webkit-transition : .2s; 537 | -moz-transition : .2s; 538 | -o-transition : .2s; 539 | transition : .2s; 540 | border-radius : 5px; 541 | -moz-border-radius : 5px; 542 | opacity : .01; 543 | } 544 | .nano:hover .pane, .pane.active { 545 | opacity : 1; 546 | } 547 | .nano .content { padding: 10px; } 548 | .nano .pane { background: #444; } 549 | .nano .pane .slider { background: #111; } 550 | 551 | body, html { 552 | overflow: hidden; 553 | } 554 | -------------------------------------------------------------------------------- /client/js/ruler.js: -------------------------------------------------------------------------------- 1 | define(["jquery", "config", "libs/big.min" ], function(jQuery, Config, Big){ 2 | // This class represents the length rulers the user draws on the screen to perform a distance measurement. 3 | // lightBox should be the Raphael-based svg object that this ruler resides on 4 | var ruler = function(lightBox, ratio, xHeaderSpacing, yHeaderSpacing){ 5 | var that = {}; 6 | var line = null; 7 | var startPoint = null; 8 | var endPoint = null; 9 | var textLoc = {}; 10 | var text = null; 11 | var slope = null; 12 | var trace = null; 13 | var end1 = null; 14 | var end2 = null; 15 | var boundingBox = null; 16 | var boundingBoxEnd1 = null; 17 | var boundingBoxEnd2 = null; 18 | var textBoundaryBox = null; 19 | 20 | // This function is based on the Raphael safari( ) call. There are some issues in Chrome 7 with the SVG DOM not updating. 21 | // We could only do this in Chrome, but it's not doing any harm. 22 | // See OV-98 23 | var redraw = function(){ 24 | var rect = lightBox.paper.rect(-99, -99, lightBox.paper.width + 99, lightBox.paper.height + 99).attr({stroke: "none"}); 25 | setTimeout(function () {rect.remove();}); 26 | }; 27 | 28 | var calculateLength = function (){ 29 | 30 | var mult = 1; 31 | var xDiff = Big(startPoint.x).minus(endPoint.x); 32 | var yDiff = Big(startPoint.y).minus(endPoint.y); 33 | 34 | xDiff = xDiff.div(ratio); 35 | yDiff = yDiff.div(ratio); 36 | 37 | var xSpacing = Big(1); 38 | var ySpacing = Big(1); 39 | // If we don't have pixelSpacing data, units will be in pixels 40 | // Note it is intentional here that 0 or null or undefined for either var will result 41 | // in false 42 | var units = "px"; 43 | if (xHeaderSpacing && yHeaderSpacing){ 44 | units = "mm"; 45 | xSpacing = Big(xHeaderSpacing); 46 | ySpacing = Big(yHeaderSpacing); 47 | } 48 | 49 | var xDistance = xSpacing.times(xDiff); 50 | var yDistance = ySpacing.times(yDiff); 51 | 52 | //var measurement = fn["sqrt"](fn["+"](fn["*"](xDistance,xDistance), fn["*"](yDistance,yDistance))); 53 | var measurement = xDistance.pow(2).plus(yDistance.pow(2)); 54 | measurement = measurement.sqrt(); 55 | measurement = measurement.toFixed(Config.MeasurementPrecision)+ units; 56 | 57 | return measurement; 58 | }; 59 | that.calculateLength = calculateLength; 60 | 61 | var start = function(point){ 62 | startPoint = point; 63 | }; 64 | that.start = start; 65 | 66 | var solver = function(px,py,length,slope){ 67 | 68 | // if slope is 0 or undefined, we can't use our regular formula 69 | if (slope === 0 ){ 70 | return [{x:px,y:py+length},{x:px,y:py-length}]; 71 | } else if (slope === undefined) { 72 | return [{x:px+length,y:py},{x:px-length,y:py}]; 73 | } else { 74 | var m = -1/slope; 75 | var x1 = (Math.pow(m,2)*px+length*Math.sqrt(Math.pow(m,2)+1)+px)/(Math.pow(m,2)+1); 76 | var x2 = (Math.pow(m,2)*px-length*Math.sqrt(Math.pow(m,2)+1)+px)/(Math.pow(m,2)+1); 77 | var y1 = m * (x1-px)+py; 78 | var y2 = m * (x2-px)+py; 79 | return [{x:parseFloat(x1.toFixed(1)),y:parseFloat(y1.toFixed(1))},{x:parseFloat(x2.toFixed(1)),y:parseFloat(y2.toFixed(1))}]; 80 | } 81 | }; 82 | 83 | var end = function(point){ 84 | endPoint = point; 85 | // Handle a quick click 86 | if (line === null){ 87 | return false; 88 | } 89 | 90 | // If was a ruler that was moved, we need to clear the line (during a move) or the whole thing (if they didn't actually move it) 91 | clear(); 92 | 93 | // Length calculations 94 | var measurement = calculateLength(); 95 | var textPlacement = 45; 96 | if (endPoint.x + 60 > lightBox.width) { 97 | textPlacement = -45; 98 | } 99 | text = lightBox.paper.text(endPoint.x+textPlacement, endPoint.y, measurement); 100 | 101 | // White text with a black border 102 | text.attr("font-size", 12); 103 | text.attr("font-weight", "normal"); 104 | text.attr("font-style","normal"); 105 | 106 | text.attr("stroke-width", 0.5); 107 | text.attr("font-family","courier"); 108 | text.attr("fill",Config.MeasurementColor); 109 | var textBox = text.getBBox(); 110 | textBoundaryBox = lightBox.paper.rect(endPoint.x+textPlacement - textBox.width/2, endPoint.y-textBox.height/2, textBox.width, textBox.height); 111 | textBoundaryBox.attr("fill", Config.HintColor); 112 | textBoundaryBox.attr("opacity",0); 113 | var textSet = lightBox.paper.set(); 114 | textSet.push(text); 115 | textSet.push(textBoundaryBox); 116 | 117 | // The user can drag the line and the text. When the text is selected, draw a dashed line to the ruler it corresponds to. 118 | if (startPoint.x === endPoint.x){ 119 | slope = undefined; 120 | } else { 121 | slope = (startPoint.y - endPoint.y)/(startPoint.x - endPoint.x); 122 | } 123 | 124 | var a = solver(startPoint.x,startPoint.y,5,slope); 125 | var b = solver(endPoint.x,endPoint.y,5,slope); 126 | var rulerPath = "M"+startPoint.x+" "+startPoint.y+"L"+endPoint.x+ " "+endPoint.y; 127 | var end1BeginPoint = {x:a[0].x,y:a[0].y}; 128 | var end1EndPoint = {x:a[1].x,y:a[1].y}; 129 | var end2BeginPoint = {x:b[0].x,y:b[0].y}; 130 | var end2EndPoint = {x:b[1].x,y:b[1].y}; 131 | var endPath1 = "M"+a[0].x+" "+a[0].y+"L"+a[1].x+" "+a[1].y; 132 | var endPath2 = "M"+b[0].x+" "+b[0].y+"L"+b[1].x+" "+b[1].y; 133 | line = lightBox.paper.path(rulerPath); 134 | line.attr("stroke", Config.MeasurementColor); 135 | end1 = lightBox.paper.path(endPath1); 136 | end1.attr("stroke", Config.MeasurementColor); 137 | end2 = lightBox.paper.path(endPath2); 138 | end2.attr("stroke", Config.MeasurementColor); 139 | 140 | // The line is very thin, but we need that for accuracy, however for useability, its needs to be easier to move and manipulate 141 | // This creates a bounding box around the middle of the line that acts as a proxy for the line on rollover and dragging 142 | // The alternative was to make LightBox aware of the equations of every single line, and do calculations on every mouse move and every click to 143 | // determine the users intent and proxmity to rulers. This solution seems more elegant and likely to be faster. 144 | a = solver(startPoint.x,startPoint.y,7,slope); 145 | b = solver(endPoint.x,endPoint.y,7,slope); 146 | boundingBox = lightBox.paper.path("M"+a[0].x+" "+a[0].y+"L"+a[1].x+" "+a[1].y+"L"+b[1].x+" "+b[1].y+"L"+b[0].x+ " "+b[0].y+"L"+a[0].x+" "+a[0].y); 147 | boundingBox.attr("opacity",0); 148 | boundingBox.attr("stroke",Config.HintColor); 149 | boundingBox.attr("fill",Config.HintColor); 150 | boundingBox.toFront(); 151 | jQuery(boundingBox.node).hover(function(){line.attr('stroke',Config.HoverColor);end1.attr('stroke',Config.HoverColor);end2.attr('stroke',Config.HoverColor);lightBox.moveCursorOn();}, 152 | function(){line.attr('stroke',Config.MeasurementColor);end1.attr('stroke',Config.MeasurementColor);end2.attr('stroke',Config.MeasurementColor);lightBox.moveCursorOff();}); 153 | 154 | // Now bounding boxes for the rule marks 155 | var oppositeSlope = null; 156 | if (slope === undefined){ 157 | oppositeSlope = 0; 158 | }else if (slope === 0){ 159 | oppositeSlope = undefined; 160 | }else{ 161 | oppositeSlope = -1/slope; 162 | } 163 | 164 | a = solver(end1BeginPoint.x, end1BeginPoint.y, 3, oppositeSlope); 165 | b = solver(end1EndPoint.x, end1EndPoint.y, 3, oppositeSlope); 166 | boundingBoxEnd1 = lightBox.paper.path("M"+a[0].x+" "+a[0].y+"L"+a[1].x+" "+a[1].y+"L"+b[1].x+" "+b[1].y+"L"+b[0].x+ " "+b[0].y+"L"+a[0].x+" "+a[0].y); 167 | boundingBoxEnd1.attr("opacity",0); 168 | boundingBoxEnd1.attr("stroke",Config.HintColor); 169 | boundingBoxEnd1.attr("fill",Config.HintColor); 170 | jQuery(boundingBoxEnd1.node).hover(function(){end1.attr('stroke',Config.HoverColor);}, function(){end1.attr('stroke',Config.MeasurementColor);}); 171 | 172 | a = solver(end2BeginPoint.x, end2BeginPoint.y, 1, oppositeSlope); 173 | b = solver(end2EndPoint.x, end2EndPoint.y, 1, oppositeSlope); 174 | boundingBoxEnd2 = lightBox.paper.path("M"+a[0].x+" "+a[0].y+"L"+a[1].x+" "+a[1].y+"L"+b[1].x+" "+b[1].y+"L"+b[0].x+ " "+b[0].y+"L"+a[0].x+" "+a[0].y); 175 | boundingBoxEnd2.attr("opacity",0); 176 | boundingBoxEnd2.attr("stroke",Config.HintColor); 177 | boundingBoxEnd2.attr("fill",Config.HintColor); 178 | jQuery(boundingBoxEnd2.node).hover(function(){end2.attr('stroke',Config.HoverColor);}, function(){end2.attr('stroke',Config.MeasurementColor);}); 179 | 180 | 181 | // Make the line draggable 182 | boundingBox.draggable(); 183 | boundingBox.dragStart = function(sX,sY){ 184 | sX=Math.floor(sX); // Firefox has a bug that causes long decimals to be returned here. 185 | sY=Math.floor(sY); 186 | var startDrag = {x:sX,y:sY}; 187 | var s = lightBox.paper.set(); 188 | s.push(line); 189 | s.push(end1); 190 | s.push(end2); 191 | s.push(boundingBoxEnd1); 192 | s.push(boundingBoxEnd2); 193 | s.push(boundingBox); 194 | // This allows us to track pixel differences ourselves 195 | s.dragFinish = function(droppped, eX, eY){ 196 | eX=Math.floor(eX); // Firefox has a bug that causes long decimals to be returned here. 197 | eY=Math.floor(eY); 198 | var dX = startDrag.x-eX; 199 | var dY = startDrag.y-eY; 200 | startPoint.x = startPoint.x-dX; 201 | startPoint.y = startPoint.y-dY; 202 | endPoint.x = endPoint.x-dX; 203 | endPoint.y = endPoint.y-dY; 204 | }; 205 | return s; 206 | }; 207 | 208 | 209 | // Make the line moveable by clicking on either end 210 | jQuery(boundingBoxEnd2.node).mousedown(function(event){ 211 | jQuery(boundingBox.node).unbind(); 212 | jQuery(boundingBoxEnd2.node).unbind(); 213 | event.stopPropagation(); 214 | event.preventDefault(); 215 | textLoc = {}; 216 | lightBox.mouseDown(null,that,startPoint.x,startPoint.y); 217 | }); 218 | 219 | jQuery(boundingBoxEnd1.node).mousedown(function(event){ 220 | jQuery(boundingBox.node).unbind(); 221 | jQuery(boundingBoxEnd1.node).unbind(); 222 | event.stopPropagation(); 223 | event.preventDefault(); 224 | textLoc = {}; 225 | lightBox.mouseDown(null,that,endPoint.x,endPoint.y); 226 | }); 227 | 228 | redraw(); 229 | // This code handles drawing a dashed line when the text for a ruler is clicked. It also makes text draggable 230 | jQuery(textBoundaryBox.node).mousedown(function(){ 231 | var tracePath = "M "+(textLoc.x || text.attr('x')) + " " +(textLoc.y || text.attr("y")) + "L"+endPoint.x + " " + endPoint.y; 232 | trace = lightBox.paper.path(tracePath); 233 | trace.attr("stroke-width", 1); 234 | trace.attr("stroke",Config.HintColor); 235 | trace.attr("stroke-dasharray",['--']); 236 | 237 | redraw(); 238 | 239 | jQuery(textBoundaryBox.node).one("mousemove", function(event) { 240 | if (trace !== null){ 241 | trace.remove(); 242 | trace = null; 243 | } 244 | }); 245 | }); 246 | 247 | jQuery(textBoundaryBox.node).mouseup(function(){ 248 | if (trace !== null){ 249 | trace.remove(); 250 | trace = null; 251 | } 252 | }); 253 | 254 | // Raphaelle used to properly update x and y attributes of dragged items 255 | // but somehwere along the line this stopped working. Now we keep track of this 256 | // ourselves. 257 | textBoundaryBox.draggable(); 258 | textBoundaryBox.dragStart = function(sX,sY){ 259 | sX=Math.floor(sX); // Firefox has a bug that causes long decimals to be returned here. 260 | sY=Math.floor(sY); 261 | var startDrag = {x:sX,y:sY}; 262 | // Once they start to drag, remove the white line if it's there 263 | if (trace !== null){ 264 | trace.remove(); 265 | trace = null; 266 | } 267 | textSet.dragFinish = function(droppped, eX, eY){ 268 | eX=Math.floor(eX); // Firefox has a bug that causes long decimals to be returned here. 269 | eY=Math.floor(eY); 270 | var dX = startDrag.x-eX; 271 | var dY = startDrag.y-eY; 272 | textLoc.x = (textLoc.x || text.attr("x"))-dX; 273 | textLoc.y = (textLoc.y || text.attr("y"))-dY; 274 | }; 275 | return textSet; 276 | }; 277 | 278 | 279 | jQuery(textBoundaryBox.node).hover(function(){lightBox.moveCursorOn();}, function(){lightBox.moveCursorOff();}); 280 | 281 | // If they double click on the line, delete it. Do we want this? 282 | jQuery(boundingBox.node).dblclick(function(){ 283 | clear(); 284 | //If they were able to delete, they had the move icon, we need to pop it off 285 | lightBox.moveCursorOff(); 286 | }); 287 | 288 | return true; 289 | }; 290 | that.end = end; 291 | 292 | var clear = function (){ 293 | line && line.remove(); 294 | line = null; 295 | end1 && end1.remove(); 296 | end1 = null; 297 | end2 && end2.remove(); 298 | end2 = null; 299 | text && text.remove(); 300 | text = null; 301 | textBoundaryBox && textBoundaryBox.remove(); 302 | textBoundaryBox = null; 303 | boundingBox && jQuery(boundingBox.node).unbind(); 304 | boundingBox && boundingBox.remove(); 305 | boundingBox = null; 306 | boundingBoxEnd1 && jQuery(boundingBoxEnd1.node).unbind(); 307 | boundingBoxEnd1 && boundingBoxEnd1.remove(); 308 | boundingBoxEnd1 = null; 309 | boundingBoxEnd2 && jQuery(boundingBoxEnd2.node).unbind(); 310 | boundingBoxEnd2 && boundingBoxEnd2.remove(); 311 | boundingBoxEnd2 = null; 312 | }; 313 | that.clear = clear; 314 | 315 | var adjust = function(point){ 316 | clear(); 317 | line = lightBox.paper.path("M"+startPoint.x+" "+startPoint.y+"L"+point.x+" "+point.y); 318 | line.attr("stroke",Config.MeasurementColor); 319 | }; 320 | that.adjust = adjust; 321 | 322 | return that; 323 | }; 324 | return ruler; 325 | }); 326 | -------------------------------------------------------------------------------- /client/css/button-styles.css: -------------------------------------------------------------------------------- 1 | .buttons { 2 | background: #ccc; 3 | padding: 20px 10px 10px 20px; 4 | border-radius: 10px; 5 | -webkit-border-radius: 10px; 6 | -moz-border-radius: 10px; 7 | float: left; 8 | margin-bottom: 40px; 9 | width: 830px; 10 | } 11 | a.button, 12 | a.icon { 13 | color: #484848; 14 | text-decoration: none; 15 | text-shadow: 0 1px 0 #fff; 16 | font: bold 12px Helvetica, Arial, sans-serif; 17 | line-height: 35px; 18 | height: 35px; 19 | display: block; 20 | float: left; 21 | margin-right: 10px; 22 | padding: 0 10px; 23 | background: #f0f0f0; 24 | background: -webkit-linear-gradient(top, #f7f7f7, #d1d1d1); 25 | background: -moz-linear-gradient(top, #f7f7f7, #d1d1d1); 26 | background: -ms-linear-gradient(top, #f7f7f7, #d1d1d1); 27 | background: -o-linear-gradient(top, #f7f7f7, #d1d1d1); 28 | border: solid 1px #7f7f7f; 29 | border-radius: 4px; 30 | -webkit-border-radius: 4px; 31 | -moz-border-radius: 4px; 32 | -moz-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 33 | -webkit-box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 34 | box-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 35 | -ms-filter: "progid:DXImageTransform.Microsoft.Shadow(Strength=1, Direction=90, Color='#ffffff')"; 36 | filter: progid:DXImageTransform.Microsoft.Shadow(Strength=1, Direction=90, Color='#ffffff'); 37 | -webkit-transition: border-color .218s; 38 | -moz-transition: border .218s; 39 | -o-transition: border-color .218s; 40 | transition: border-color .218s; 41 | } 42 | a.button:hover, 43 | a.icon:hover { 44 | border: solid 1px #333; 45 | color: #282828; 46 | background: -webkit-linear-gradient(top, #ffffff, #dfdfdf); 47 | background: -moz-linear-gradient(top, #ffffff, #dfdfdf); 48 | background: -ms-linear-gradient(top, #ffffff, #dfdfdf); 49 | background: -o-linear-gradient(top, #ffffff, #dfdfdf); 50 | } 51 | a.selected, 52 | a.button:active, 53 | a.icon:active { 54 | border: solid 1px #7f7f7f; 55 | background: #d0d0d0; 56 | background: -webkit-gradient(linear, left top, left bottom, from(#a8a8a8), color-stop(0.15, #c6c6c6), to(#d8d8d8)); 57 | background: -webkit-linear-gradient(top, #a8a8a8, #c6c6c6 15%, #d8d8d8); 58 | background: -moz-linear-gradient(top, #a8a8a8, #c6c6c6 15%, #d8d8d8); 59 | background: -ms-linear-gradient(top, #a8a8a8, #c6c6c6 15%, #d8d8d8); 60 | background: -o-linear-gradient(top, #a8a8a8, #c6c6c6 15%, #d8d8d8); 61 | } 62 | 63 | a.disabled{ 64 | background: -webkit-gradient(linear, left top, left bottom, from(#3388ee), color-stop(0.15, #2a99ee), to(#2a6fe2)); 65 | background: -webkit-linear-gradient(top, #3388ee, #2a99ee 15%, #2a6fe2); 66 | background: -moz-linear-gradient(top, #3388ee, #2a99ee 15%, #2a6fe2); 67 | background: -ms-linear-gradient(top, #3388ee, #2a99ee 15%, #2a6fe2); 68 | background: -o-linear-gradient(top, #3388ee, #2a99ee 15%, #2a6fe2); 69 | } 70 | a.icon { 71 | padding: 0; 72 | margin-bottom: 10px; 73 | } 74 | a.icon span { 75 | display: block; 76 | line-height: 35px; 77 | height: 35px; 78 | width: 36px; 79 | max-width: 36px; 80 | overflow: hidden; 81 | text-indent: -9999px; 82 | } 83 | a.left { 84 | margin: 0; 85 | border-right: solid 1px #b0b0b0; 86 | -webkit-border-top-right-radius: 0; 87 | -moz-border-radius-topright: 0; 88 | border-top-right-radius: 0; 89 | -webkit-border-bottom-right-radius: 0; 90 | -moz-border-radius-bottomright: 0; 91 | border-bottom-right-radius: 0; 92 | } 93 | a.middle { 94 | border-radius: 0; 95 | -webkit-border-radius: 0; 96 | -moz-border-radius: 0; 97 | margin: 0; 98 | border-left: solid 1px rgba(255, 255, 255, 0); 99 | border-right: solid 1px #b0b0b0; 100 | } 101 | a.middle:hover, 102 | a.right:hover { border-left: solid 1px #333 } 103 | a.middle:active, 104 | a.right:active { border-left: solid 1px #7f7f7f } 105 | a.right { 106 | border-left: solid 1px rgba(255, 255, 255, 0); 107 | -webkit-border-top-left-radius: 0; 108 | -moz-border-radius-topleft: 0; 109 | border-top-left-radius: 0; 110 | -webkit-border-bottom-left-radius: 0; 111 | -moz-border-radius-bottomleft: 0; 112 | border-bottom-left-radius: 0; 113 | } 114 | 115 | a.icon1 span {background:url(button-icons.png) -0px -36px;} 116 | a.icon2 span {background:url(button-icons.png) -36px -36px;} 117 | a.icon3 span {background:url(button-icons.png) -72px -36px;} 118 | a.icon4 span {background:url(button-icons.png) -108px -36px;} 119 | a.icon5 span {background:url(button-icons.png) -144px -36px;} 120 | a.icon6 span {background:url(button-icons.png) -180px -36px;} 121 | a.icon7 span {background:url(button-icons.png) -216px -36px;} 122 | a.icon8 span {background:url(button-icons.png) -252px -36px;} 123 | a.icon9 span {background:url(button-icons.png) -288px -36px;} 124 | a.icon10 span {background:url(button-icons.png) -324px -36px;} 125 | a.icon11 span {background:url(button-icons.png) -0px -72px;} 126 | a.icon12 span {background:url(button-icons.png) -36px -72px;} 127 | a.icon13 span {background:url(button-icons.png) -72px -72px;} 128 | a.icon14 span {background:url(button-icons.png) -108px -72px;} 129 | a.icon15 span {background:url(button-icons.png) -144px -72px;} 130 | a.icon16 span {background:url(button-icons.png) -180px -72px;} 131 | a.icon17 span {background:url(button-icons.png) -216px -72px;} 132 | a.icon18 span {background:url(button-icons.png) -252px -72px;} 133 | a.icon19 span {background:url(button-icons.png) -288px -72px;} 134 | a.icon20 span {background:url(button-icons.png) -324px -72px;} 135 | a.icon21 span {background:url(button-icons.png) -0px -108px;} 136 | a.icon22 span {background:url(button-icons.png) -36px -108px;} 137 | a.icon23 span {background:url(button-icons.png) -72px -108px;} 138 | a.icon24 span {background:url(button-icons.png) -108px -108px;} 139 | a.icon25 span {background:url(button-icons.png) -144px -108px;} 140 | a.icon26 span {background:url(button-icons.png) -180px -108px;} 141 | a.icon27 span {background:url(button-icons.png) -216px -108px;} 142 | a.icon28 span {background:url(button-icons.png) -252px -108px;} 143 | a.icon29 span {background:url(button-icons.png) -288px -108px;} 144 | a.icon30 span {background:url(button-icons.png) -324px -108px;} 145 | a.icon31 span {background:url(button-icons.png) -0px -144px;} 146 | a.icon32 span {background:url(button-icons.png) -36px -144px;} 147 | a.icon33 span {background:url(button-icons.png) -72px -144px;} 148 | a.icon34 span {background:url(button-icons.png) -108px -144px;} 149 | a.icon35 span {background:url(button-icons.png) -144px -144px;} 150 | a.icon36 span {background:url(button-icons.png) -180px -144px;} 151 | a.icon37 span {background:url(button-icons.png) -216px -144px;} 152 | a.icon38 span {background:url(button-icons.png) -252px -144px;} 153 | a.icon39 span {background:url(button-icons.png) -288px -144px;} 154 | a.icon40 span {background:url(button-icons.png) -324px -144px;} 155 | a.icon41 span {background:url(button-icons.png) -0px -180px;} 156 | a.icon42 span {background:url(button-icons.png) -36px -180px;} 157 | a.icon43 span {background:url(button-icons.png) -72px -180px;} 158 | a.icon44 span {background:url(button-icons.png) -108px -180px;} 159 | a.icon45 span {background:url(button-icons.png) -144px -180px;} 160 | a.icon46 span {background:url(button-icons.png) -180px -180px;} 161 | a.icon47 span {background:url(button-icons.png) -216px -180px;} 162 | a.icon48 span {background:url(button-icons.png) -252px -180px;} 163 | a.icon49 span {background:url(button-icons.png) -288px -180px;} 164 | a.icon50 span {background:url(button-icons.png) -324px -180px;} 165 | a.icon51 span {background:url(button-icons.png) -0px -216px;} 166 | a.icon52 span {background:url(button-icons.png) -36px -216px;} 167 | a.icon53 span {background:url(button-icons.png) -72px -216px;} 168 | a.icon54 span {background:url(button-icons.png) -108px -216px;} 169 | a.icon55 span {background:url(button-icons.png) -144px -216px;} 170 | a.icon56 span {background:url(button-icons.png) -180px -216px;} 171 | a.icon57 span {background:url(button-icons.png) -216px -216px;} 172 | a.icon58 span {background:url(button-icons.png) -252px -216px;} 173 | a.icon59 span {background:url(button-icons.png) -288px -216px;} 174 | a.icon60 span {background:url(button-icons.png) -324px -216px;} 175 | a.icon61 span {background:url(button-icons.png) -0px -252px;} 176 | a.icon62 span {background:url(button-icons.png) -36px -252px;} 177 | a.icon63 span {background:url(button-icons.png) -72px -252px;} 178 | a.icon64 span {background:url(button-icons.png) -108px -252px;} 179 | a.icon65 span {background:url(button-icons.png) -144px -252px;} 180 | a.icon66 span {background:url(button-icons.png) -180px -252px;} 181 | a.icon67 span {background:url(button-icons.png) -216px -252px;} 182 | a.icon68 span {background:url(button-icons.png) -252px -252px;} 183 | a.icon69 span {background:url(button-icons.png) -288px -252px;} 184 | a.icon70 span {background:url(button-icons.png) -324px -252px;} 185 | a.icon71 span {background:url(button-icons.png) -0px -288px;} 186 | a.icon72 span {background:url(button-icons.png) -36px -288px;} 187 | a.icon73 span {background:url(button-icons.png) -72px -288px;} 188 | a.icon74 span {background:url(button-icons.png) -108px -288px;} 189 | a.icon75 span {background:url(button-icons.png) -144px -288px;} 190 | a.icon76 span {background:url(button-icons.png) -180px -288px;} 191 | a.icon77 span {background:url(button-icons.png) -216px -288px;} 192 | a.icon78 span {background:url(button-icons.png) -252px -288px;} 193 | a.icon79 span {background:url(button-icons.png) -288px -288px;} 194 | a.icon80 span {background:url(button-icons.png) -324px -288px;} 195 | a.icon81 span {background:url(button-icons.png) -0px -324px;} 196 | a.icon82 span {background:url(button-icons.png) -36px -324px;} 197 | a.icon83 span {background:url(button-icons.png) -72px -324px;} 198 | a.icon84 span {background:url(button-icons.png) -108px -324px;} 199 | a.icon85 span {background:url(button-icons.png) -144px -324px;} 200 | a.icon86 span {background:url(button-icons.png) -180px -324px;} 201 | a.icon87 span {background:url(button-icons.png) -216px -324px;} 202 | a.icon88 span {background:url(button-icons.png) -252px -324px;} 203 | a.icon89 span {background:url(button-icons.png) -288px -324px;} 204 | a.icon90 span {background:url(button-icons.png) -324px -324px;} 205 | a.icon91 span {background:url(button-icons.png) -0px -360px;} 206 | a.icon92 span {background:url(button-icons.png) -36px -360px;} 207 | a.icon93 span {background:url(button-icons.png) -72px -360px;} 208 | a.icon94 span {background:url(button-icons.png) -108px -360px;} 209 | a.icon95 span {background:url(button-icons.png) -144px -360px;} 210 | a.icon96 span {background:url(button-icons.png) -180px -360px;} 211 | a.icon97 span {background:url(button-icons.png) -216px -360px;} 212 | a.icon98 span {background:url(button-icons.png) -252px -360px;} 213 | a.icon99 span {background:url(button-icons.png) -288px -360px;} 214 | a.icon100 span {background:url(button-icons.png) -324px -360px;} 215 | a.icon101 span {background:url(button-icons.png) -0px -396px;} 216 | a.icon102 span {background:url(button-icons.png) -36px -396px;} 217 | a.icon103 span {background:url(button-icons.png) -72px -396px;} 218 | a.icon104 span {background:url(button-icons.png) -108px -396px;} 219 | a.icon105 span {background:url(button-icons.png) -144px -396px;} 220 | a.icon106 span {background:url(button-icons.png) -180px -396px;} 221 | a.icon107 span {background:url(button-icons.png) -216px -396px;} 222 | a.icon108 span {background:url(button-icons.png) -252px -396px;} 223 | a.icon109 span {background:url(button-icons.png) -288px -396px;} 224 | a.icon110 span {background:url(button-icons.png) -324px -396px;} 225 | a.icon111 span {background:url(button-icons.png) -0px -432px;} 226 | a.icon112 span {background:url(button-icons.png) -36px -432px;} 227 | a.icon113 span {background:url(button-icons.png) -72px -432px;} 228 | a.icon114 span {background:url(button-icons.png) -108px -432px;} 229 | a.icon115 span {background:url(button-icons.png) -144px -432px;} 230 | a.icon116 span {background:url(button-icons.png) -180px -432px;} 231 | a.icon117 span {background:url(button-icons.png) -216px -432px;} 232 | a.icon118 span {background:url(button-icons.png) -252px -432px;} 233 | a.icon119 span {background:url(button-icons.png) -288px -432px;} 234 | a.icon120 span {background:url(button-icons.png) -324px -432px;} 235 | a.icon121 span {background:url(button-icons.png) -0px -468px;} 236 | a.icon122 span {background:url(button-icons.png) -36px -468px;} 237 | a.icon123 span {background:url(button-icons.png) -72px -468px;} 238 | a.icon124 span {background:url(button-icons.png) -108px -468px;} 239 | a.icon125 span {background:url(button-icons.png) -144px -468px;} 240 | a.icon126 span {background:url(button-icons.png) -180px -468px;} 241 | a.icon127 span {background:url(button-icons.png) -216px -468px;} 242 | a.icon128 span {background:url(button-icons.png) -252px -468px;} 243 | a.icon129 span {background:url(button-icons.png) -288px -468px;} 244 | a.icon130 span {background:url(button-icons.png) -324px -468px;} 245 | a.icon131 span {background:url(button-icons.png) -0px -504px;} 246 | a.icon132 span {background:url(button-icons.png) -36px -504px;} 247 | a.icon133 span {background:url(button-icons.png) -72px -504px;} 248 | a.icon134 span {background:url(button-icons.png) -108px -504px;} 249 | a.icon135 span {background:url(button-icons.png) -144px -504px;} 250 | a.icon136 span {background:url(button-icons.png) -180px -504px;} 251 | a.icon137 span {background:url(button-icons.png) -216px -504px;} 252 | a.icon138 span {background:url(button-icons.png) -252px -504px;} 253 | a.icon139 span {background:url(button-icons.png) -288px -504px;} 254 | a.icon140 span {background:url(button-icons.png) -324px -504px;} 255 | a.icon141 span {background:url(button-icons.png) -0px -540px;} 256 | a.icon142 span {background:url(button-icons.png) -36px -540px;} 257 | a.icon143 span {background:url(button-icons.png) -72px -540px;} 258 | a.icon144 span {background:url(button-icons.png) -108px -540px;} 259 | a.icon145 span {background:url(button-icons.png) -144px -540px;} 260 | a.icon146 span {background:url(button-icons.png) -180px -540px;} 261 | a.icon147 span {background:url(button-icons.png) -216px -540px;} 262 | a.icon148 span {background:url(button-icons.png) -252px -540px;} 263 | a.icon149 span {background:url(button-icons.png) -288px -540px;} 264 | a.icon150 span {background:url(button-icons.png) -324px -540px;} 265 | a.icon151 span {background:url(button-icons.png) -0px -576px;} 266 | a.icon152 span {background:url(button-icons.png) -36px -576px;} 267 | a.icon153 span {background:url(button-icons.png) -72px -576px;} 268 | a.icon154 span {background:url(button-icons.png) -108px -576px;} 269 | a.icon155 span {background:url(button-icons.png) -144px -576px;} 270 | a.icon156 span {background:url(button-icons.png) -180px -576px;} 271 | a.icon157 span {background:url(button-icons.png) -216px -576px;} 272 | a.icon158 span {background:url(button-icons.png) -252px -576px;} 273 | a.icon159 span {background:url(button-icons.png) -288px -576px;} 274 | a.icon160 span {background:url(button-icons.png) -324px -576px;} 275 | a.icon161 span {background:url(button-icons.png) -0px -612px;} 276 | a.icon162 span {background:url(button-icons.png) -36px -612px;} 277 | a.icon163 span {background:url(button-icons.png) -72px -612px;} 278 | a.icon164 span {background:url(button-icons.png) -108px -612px;} 279 | a.icon165 span {background:url(button-icons.png) -144px -612px;} 280 | a.icon166 span {background:url(button-icons.png) -180px -612px;} 281 | a.icon167 span {background:url(button-icons.png) -216px -612px;} 282 | a.icon168 span {background:url(button-icons.png) -252px -612px;} 283 | a.icon169 span {background:url(button-icons.png) -288px -612px;} 284 | a.icon170 span {background:url(button-icons.png) -324px -612px;} 285 | a.icon171 span {background:url(button-icons.png) -0px -648px;} 286 | a.icon172 span {background:url(button-icons.png) -36px -648px;} 287 | a.icon173 span {background:url(button-icons.png) -72px -648px;} 288 | a.icon174 span {background:url(button-icons.png) -108px -648px;} 289 | a.icon175 span {background:url(button-icons.png) -144px -648px;} 290 | a.icon176 span {background:url(button-icons.png) -180px -648px;} 291 | a.icon177 span {background:url(button-icons.png) -216px -648px;} 292 | a.icon178 span {background:url(button-icons.png) -252px -648px;} 293 | a.icon179 span {background:url(button-icons.png) -288px -648px;} 294 | a.icon180 span {background:url(button-icons.png) -324px -648px;} 295 | a.icon181 span {background:url(button-icons.png) -0px -684px;} 296 | a.icon182 span {background:url(button-icons.png) -36px -684px;} 297 | a.icon183 span {background:url(button-icons.png) -72px -684px;} 298 | a.icon184 span {background:url(button-icons.png) -108px -684px;} 299 | a.icon185 span {background:url(button-icons.png) -144px -684px;} 300 | a.icon186 span {background:url(button-icons.png) -180px -684px;} 301 | a.icon187 span {background:url(button-icons.png) -216px -684px;} 302 | a.icon188 span {background:url(button-icons.png) -252px -684px;} 303 | a.icon189 span {background:url(button-icons.png) -288px -684px;} 304 | a.icon190 span {background:url(button-icons.png) -324px -684px;} 305 | a.icon191 span {background:url(button-icons.png) -0px -720px;} 306 | a.icon192 span {background:url(button-icons.png) -36px -720px;} 307 | a.icon193 span {background:url(button-icons.png) -72px -720px;} 308 | a.icon194 span {background:url(button-icons.png) -108px -720px;} 309 | a.icon195 span {background:url(button-icons.png) -144px -720px;} 310 | a.icon196 span {background:url(button-icons.png) -180px -720px;} 311 | a.icon197 span {background:url(button-icons.png) -216px -720px;} 312 | a.icon198 span {background:url(button-icons.png) -252px -720px;} 313 | a.icon199 span {background:url(button-icons.png) -288px -720px;} 314 | a.icon200 span {background:url(button-icons.png) -324px -720px;} 315 | -------------------------------------------------------------------------------- /client/css/Aristo/jquery-ui-1.8.7.custom.css: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery UI CSS Framework 1.8.7 3 | * 4 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * http://jquery.org/license 7 | * 8 | * http://docs.jquery.com/UI/Theming/API 9 | */ 10 | 11 | /* Layout helpers 12 | ----------------------------------*/ 13 | .ui-helper-hidden { display: none; } 14 | .ui-helper-hidden-accessible { position: absolute !important; clip: rect(1px 1px 1px 1px); clip: rect(1px,1px,1px,1px); } 15 | .ui-helper-reset { margin: 0; padding: 0; border: 0; outline: 0; line-height: 1.3; text-decoration: none; font-size: 100%; list-style: none; } 16 | .ui-helper-clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } 17 | .ui-helper-clearfix { display: inline-block; } 18 | /* required comment for clearfix to work in Opera \*/ 19 | * html .ui-helper-clearfix { height:1%; } 20 | .ui-helper-clearfix { display:block; } 21 | /* end clearfix */ 22 | .ui-helper-zfix { width: 100%; height: 100%; top: 0; left: 0; position: absolute; opacity: 0; filter:Alpha(Opacity=0); } 23 | 24 | 25 | /* Interaction Cues 26 | ----------------------------------*/ 27 | .ui-state-disabled { cursor: default !important; } 28 | 29 | 30 | /* Icons 31 | ----------------------------------*/ 32 | 33 | /* states and images */ 34 | .ui-icon { display: block; text-indent: -99999px; overflow: hidden; background-repeat: no-repeat; } 35 | 36 | 37 | /* Misc visuals 38 | ----------------------------------*/ 39 | 40 | /* Overlays */ 41 | .ui-widget-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } 42 | 43 | 44 | /* 45 | * jQuery UI CSS Framework 1.8.7 46 | * 47 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 48 | * Dual licensed under the MIT or GPL Version 2 licenses. 49 | * http://jquery.org/license 50 | * 51 | * http://docs.jquery.com/UI/Theming/API 52 | * 53 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ctl=themeroller 54 | */ 55 | 56 | 57 | /* Component containers 58 | ----------------------------------*/ 59 | .ui-widget { font-family: Helvetica,Arial,sans-serif; font-size: 1.1em; } 60 | .ui-widget .ui-widget { font-size: 1em; } 61 | .ui-widget input, .ui-widget select, .ui-widget textarea, .ui-widget button { font-family: Helvetica,Arial,sans-serif; font-size: 1em; } 62 | .ui-widget-content { border: 1px solid #B6B6B6; background: #ffffff; color: #4F4F4F; } 63 | .ui-widget-content a { color: #4F4F4F; } 64 | .ui-widget-header { border: 1px solid #B6B6B6; color: #4F4F4F; font-weight: bold; } 65 | .ui-widget-header { 66 | background: url(images/bg_fallback.png) 0 0 repeat-x; 67 | background: -webkit-gradient( 68 | linear, 69 | left bottom, 70 | left top, 71 | color-stop(1, rgb(237,237,237)), 72 | color-stop(0, rgb(196,196,196)) 73 | ); 74 | background: -moz-linear-gradient( 75 | center top, 76 | rgb(237,237,237), 77 | rgb(196,196,196) 78 | ); 79 | } 80 | .ui-widget-header a { color: #4F4F4F; } 81 | 82 | /* Interaction states 83 | ----------------------------------*/ 84 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { border: 1px solid #B6B6B6; font-weight: normal; color: #4F4F4F; } 85 | .ui-state-default, .ui-widget-content .ui-state-default, .ui-widget-header .ui-state-default { 86 | background: url(images/bg_fallback.png) 0 0 repeat-x; 87 | background: -webkit-gradient( 88 | linear, 89 | left bottom, 90 | left top, 91 | color-stop(1, rgb(237,237,237)), 92 | color-stop(0, rgb(196,196,196)) 93 | ); 94 | background: -moz-linear-gradient( 95 | center top, 96 | rgb(237,237,237), 97 | rgb(196,196,196) 98 | ); 99 | -webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; 100 | -moz-box-shadow: 0 1px 0 rgba(255,255,255,0.6) inset; 101 | } 102 | .ui-state-default a, .ui-state-default a:link, .ui-state-default a:visited { color: #4F4F4F; text-decoration: none; } 103 | .ui-state-hover, .ui-widget-content .ui-state-hover, .ui-widget-header .ui-state-hover, .ui-state-focus, .ui-widget-content .ui-state-focus, .ui-widget-header .ui-state-focus { border: 1px solid #9D9D9D; font-weight: normal; color: #313131; } 104 | .ui-state-hover a, .ui-state-hover a:hover { color: #313131; text-decoration: none; } 105 | .ui-state-active, .ui-widget-content .ui-state-active, .ui-widget-header .ui-state-active { 106 | outline: none; 107 | color: #1c4257; border: 1px solid #7096ab; 108 | background: url(images/bg_fallback.png) 0 -50px repeat-x; 109 | background: -webkit-gradient( 110 | linear, 111 | left bottom, 112 | left top, 113 | color-stop(1, rgb(185,224,245)), 114 | color-stop(0, rgb(146,189,214)) 115 | ); 116 | background: -moz-linear-gradient( 117 | center top, 118 | rgb(185,224,245), 119 | rgb(146,189,214) 120 | ); 121 | -webkit-box-shadow: none; 122 | -moz-box-shadow: none; 123 | } 124 | .ui-state-active a, .ui-state-active a:link, .ui-state-active a:visited { color: #313131; text-decoration: none; } 125 | .ui-widget :active { outline: none; } 126 | 127 | /* Interaction Cues 128 | ----------------------------------*/ 129 | .ui-state-highlight, .ui-widget-content .ui-state-highlight, .ui-widget-header .ui-state-highlight { border: 1px solid #d2dbf4; background: #f4f8fd; color: #0d2054; -moz-border-radius: 0 !important; -webkit-border-radius: 0 !important; border-radius: 0 !important; } 130 | .ui-state-highlight a, .ui-widget-content .ui-state-highlight a,.ui-widget-header .ui-state-highlight a { color: #363636; } 131 | .ui-state-error, .ui-widget-content .ui-state-error, .ui-widget-header .ui-state-error { border: 1px solid #e2d0d0; background: #fcf0f0; color: #280b0b; -moz-border-radius: 0 !important; -webkit-border-radius: 0 !important; border-radius: 0 !important; } 132 | .ui-state-error a, .ui-widget-content .ui-state-error a, .ui-widget-header .ui-state-error a { color: #cd0a0a; } 133 | .ui-state-error-text, .ui-widget-content .ui-state-error-text, .ui-widget-header .ui-state-error-text { color: #cd0a0a; } 134 | .ui-priority-primary, .ui-widget-content .ui-priority-primary, .ui-widget-header .ui-priority-primary { font-weight: bold; } 135 | .ui-priority-secondary, .ui-widget-content .ui-priority-secondary, .ui-widget-header .ui-priority-secondary { opacity: .7; filter:Alpha(Opacity=70); font-weight: normal; } 136 | .ui-state-disabled, .ui-widget-content .ui-state-disabled, .ui-widget-header .ui-state-disabled { opacity: .35; filter:Alpha(Opacity=35); background-image: none; } 137 | 138 | /* Icons 139 | ----------------------------------*/ 140 | 141 | /* states and images */ 142 | .ui-icon { width: 16px; height: 16px; background-image: url(images/ui-icons_222222_256x240.png); } 143 | .ui-widget-content .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } 144 | .ui-widget-header .ui-icon {background-image: url(images/ui-icons_222222_256x240.png); } 145 | .ui-state-default .ui-icon { background-image: url(images/ui-icons_454545_256x240.png); } 146 | .ui-state-hover .ui-icon, .ui-state-focus .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } 147 | .ui-state-active .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } 148 | .ui-state-highlight .ui-icon {background-image: url(images/ui-icons_454545_256x240.png); } 149 | .ui-state-error .ui-icon, .ui-state-error-text .ui-icon { background: url(images/icon_sprite.png) -16px 0 no-repeat !important; } 150 | .ui-state-highlight .ui-icon, .ui-state-error .ui-icon { margin-top: -1px; } 151 | 152 | /* positioning */ 153 | .ui-icon-carat-1-n { background-position: 0 0; } 154 | .ui-icon-carat-1-ne { background-position: -16px 0; } 155 | .ui-icon-carat-1-e { background-position: -32px 0; } 156 | .ui-icon-carat-1-se { background-position: -48px 0; } 157 | .ui-icon-carat-1-s { background-position: -64px 0; } 158 | .ui-icon-carat-1-sw { background-position: -80px 0; } 159 | .ui-icon-carat-1-w { background-position: -96px 0; } 160 | .ui-icon-carat-1-nw { background-position: -112px 0; } 161 | .ui-icon-carat-2-n-s { background-position: -128px 0; } 162 | .ui-icon-carat-2-e-w { background-position: -144px 0; } 163 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 164 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 165 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 166 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 167 | .ui-icon-triangle-1-s { background-position: -64px -16px; } 168 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 169 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 170 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 171 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 172 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 173 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 174 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 175 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 176 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 177 | .ui-icon-arrow-1-s { background-position: -64px -32px; } 178 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 179 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 180 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 181 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 182 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 183 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 184 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 185 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 186 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 187 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 188 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 189 | .ui-icon-arrowthick-1-n { background-position: 0 -48px; } 190 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 191 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 192 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 193 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 194 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 195 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 196 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 197 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 198 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 199 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 200 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 201 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 202 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 203 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 204 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 205 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 206 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 207 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 208 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 209 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 210 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 211 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 212 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 213 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 214 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 215 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 216 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 217 | .ui-icon-arrow-4 { background-position: 0 -80px; } 218 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 219 | .ui-icon-extlink { background-position: -32px -80px; } 220 | .ui-icon-newwin { background-position: -48px -80px; } 221 | .ui-icon-refresh { background-position: -64px -80px; } 222 | .ui-icon-shuffle { background-position: -80px -80px; } 223 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 224 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 225 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 226 | .ui-icon-folder-open { background-position: -16px -96px; } 227 | .ui-icon-document { background-position: -32px -96px; } 228 | .ui-icon-document-b { background-position: -48px -96px; } 229 | .ui-icon-note { background-position: -64px -96px; } 230 | .ui-icon-mail-closed { background-position: -80px -96px; } 231 | .ui-icon-mail-open { background-position: -96px -96px; } 232 | .ui-icon-suitcase { background-position: -112px -96px; } 233 | .ui-icon-comment { background-position: -128px -96px; } 234 | .ui-icon-person { background-position: -144px -96px; } 235 | .ui-icon-print { background-position: -160px -96px; } 236 | .ui-icon-trash { background-position: -176px -96px; } 237 | .ui-icon-locked { background-position: -192px -96px; } 238 | .ui-icon-unlocked { background-position: -208px -96px; } 239 | .ui-icon-bookmark { background-position: -224px -96px; } 240 | .ui-icon-tag { background-position: -240px -96px; } 241 | .ui-icon-home { background-position: 0 -112px; } 242 | .ui-icon-flag { background-position: -16px -112px; } 243 | .ui-icon-calendar { background-position: -32px -112px; } 244 | .ui-icon-cart { background-position: -48px -112px; } 245 | .ui-icon-pencil { background-position: -64px -112px; } 246 | .ui-icon-clock { background-position: -80px -112px; } 247 | .ui-icon-disk { background-position: -96px -112px; } 248 | .ui-icon-calculator { background-position: -112px -112px; } 249 | .ui-icon-zoomin { background-position: -128px -112px; } 250 | .ui-icon-zoomout { background-position: -144px -112px; } 251 | .ui-icon-search { background-position: -160px -112px; } 252 | .ui-icon-wrench { background-position: -176px -112px; } 253 | .ui-icon-gear { background-position: -192px -112px; } 254 | .ui-icon-heart { background-position: -208px -112px; } 255 | .ui-icon-star { background-position: -224px -112px; } 256 | .ui-icon-link { background-position: -240px -112px; } 257 | .ui-icon-cancel { background-position: 0 -128px; } 258 | .ui-icon-plus { background-position: -16px -128px; } 259 | .ui-icon-plusthick { background-position: -32px -128px; } 260 | .ui-icon-minus { background-position: -48px -128px; } 261 | .ui-icon-minusthick { background-position: -64px -128px; } 262 | .ui-icon-close { background-position: -80px -128px; } 263 | .ui-icon-closethick { background-position: -96px -128px; } 264 | .ui-icon-key { background-position: -112px -128px; } 265 | .ui-icon-lightbulb { background-position: -128px -128px; } 266 | .ui-icon-scissors { background-position: -144px -128px; } 267 | .ui-icon-clipboard { background-position: -160px -128px; } 268 | .ui-icon-copy { background-position: -176px -128px; } 269 | .ui-icon-contact { background-position: -192px -128px; } 270 | .ui-icon-image { background-position: -208px -128px; } 271 | .ui-icon-video { background-position: -224px -128px; } 272 | .ui-icon-script { background-position: -240px -128px; } 273 | .ui-icon-alert { background-position: 0 -144px; } 274 | .ui-icon-info { background: url(images/icon_sprite.png) 0 0 no-repeat !important; } 275 | .ui-icon-notice { background-position: -32px -144px; } 276 | .ui-icon-help { background-position: -48px -144px; } 277 | .ui-icon-check { background-position: -64px -144px; } 278 | .ui-icon-bullet { background-position: -80px -144px; } 279 | .ui-icon-radio-off { background-position: -96px -144px; } 280 | .ui-icon-radio-on { background-position: -112px -144px; } 281 | .ui-icon-pin-w { background-position: -128px -144px; } 282 | .ui-icon-pin-s { background-position: -144px -144px; } 283 | .ui-icon-play { background-position: 0 -160px; } 284 | .ui-icon-pause { background-position: -16px -160px; } 285 | .ui-icon-seek-next { background-position: -32px -160px; } 286 | .ui-icon-seek-prev { background-position: -48px -160px; } 287 | .ui-icon-seek-end { background-position: -64px -160px; } 288 | .ui-icon-seek-start { background-position: -80px -160px; } 289 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 290 | .ui-icon-seek-first { background-position: -80px -160px; } 291 | .ui-icon-stop { background-position: -96px -160px; } 292 | .ui-icon-eject { background-position: -112px -160px; } 293 | .ui-icon-volume-off { background-position: -128px -160px; } 294 | .ui-icon-volume-on { background-position: -144px -160px; } 295 | .ui-icon-power { background-position: 0 -176px; } 296 | .ui-icon-signal-diag { background-position: -16px -176px; } 297 | .ui-icon-signal { background-position: -32px -176px; } 298 | .ui-icon-battery-0 { background-position: -48px -176px; } 299 | .ui-icon-battery-1 { background-position: -64px -176px; } 300 | .ui-icon-battery-2 { background-position: -80px -176px; } 301 | .ui-icon-battery-3 { background-position: -96px -176px; } 302 | .ui-icon-circle-plus { background-position: 0 -192px; } 303 | .ui-icon-circle-minus { background-position: -16px -192px; } 304 | .ui-icon-circle-close { background-position: -32px -192px; } 305 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 306 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 307 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 308 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 309 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 310 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 311 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 312 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 313 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 314 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 315 | .ui-icon-circle-check { background-position: -208px -192px; } 316 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 317 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 318 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 319 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 320 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 321 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 322 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 323 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 324 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 325 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 326 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 327 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 328 | 329 | 330 | /* Misc visuals 331 | ----------------------------------*/ 332 | 333 | /* Corner radius */ 334 | .ui-corner-tl { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; } 335 | .ui-corner-tr { -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; } 336 | .ui-corner-bl { -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; } 337 | .ui-corner-br { -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; } 338 | .ui-corner-top { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; } 339 | .ui-corner-bottom { -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; } 340 | .ui-corner-right { -moz-border-radius-topright: 3px; -webkit-border-top-right-radius: 3px; border-top-right-radius: 3px; -moz-border-radius-bottomright: 3px; -webkit-border-bottom-right-radius: 3px; border-bottom-right-radius: 3px; } 341 | .ui-corner-left { -moz-border-radius-topleft: 3px; -webkit-border-top-left-radius: 3px; border-top-left-radius: 3px; -moz-border-radius-bottomleft: 3px; -webkit-border-bottom-left-radius: 3px; border-bottom-left-radius: 3px; } 342 | .ui-corner-all { -moz-border-radius: 3px; -webkit-border-radius: 3px; border-radius: 3px; } 343 | 344 | /* Overlays */ 345 | .ui-widget-overlay { background: #262b33; opacity: .70;filter:Alpha(Opacity=70); } 346 | .ui-widget-shadow { margin: -8px 0 0 -8px; padding: 8px; background: #000000; opacity: .30;filter:Alpha(Opacity=30); -moz-border-radius: 8px; -webkit-border-radius: 8px; border-radius: 8px; }/* 347 | * jQuery UI Resizable 1.8.7 348 | * 349 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 350 | * Dual licensed under the MIT or GPL Version 2 licenses. 351 | * http://jquery.org/license 352 | * 353 | * http://docs.jquery.com/UI/Resizable#theming 354 | */ 355 | .ui-resizable { position: relative;} 356 | .ui-resizable-handle { position: absolute;font-size: 0.1px;z-index: 99999; display: block;} 357 | .ui-resizable-disabled .ui-resizable-handle, .ui-resizable-autohide .ui-resizable-handle { display: none; } 358 | .ui-resizable-n { cursor: n-resize; height: 7px; width: 100%; top: -5px; left: 0; } 359 | .ui-resizable-s { cursor: s-resize; height: 7px; width: 100%; bottom: -5px; left: 0; } 360 | .ui-resizable-e { cursor: e-resize; width: 7px; right: -5px; top: 0; height: 100%; } 361 | .ui-resizable-w { cursor: w-resize; width: 7px; left: -5px; top: 0; height: 100%; } 362 | .ui-resizable-se { cursor: se-resize; width: 12px; height: 12px; right: 1px; bottom: 1px; } 363 | .ui-resizable-sw { cursor: sw-resize; width: 9px; height: 9px; left: -5px; bottom: -5px; } 364 | .ui-resizable-nw { cursor: nw-resize; width: 9px; height: 9px; left: -5px; top: -5px; } 365 | .ui-resizable-ne { cursor: ne-resize; width: 9px; height: 9px; right: -5px; top: -5px;}/* 366 | * jQuery UI Selectable 1.8.7 367 | * 368 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 369 | * Dual licensed under the MIT or GPL Version 2 licenses. 370 | * http://jquery.org/license 371 | * 372 | * http://docs.jquery.com/UI/Selectable#theming 373 | */ 374 | .ui-selectable-helper { position: absolute; z-index: 100; border:1px dotted black; } 375 | /* 376 | * jQuery UI Accordion 1.8.7 377 | * 378 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 379 | * Dual licensed under the MIT or GPL Version 2 licenses. 380 | * http://jquery.org/license 381 | * 382 | * http://docs.jquery.com/UI/Accordion#theming 383 | */ 384 | /* IE/Win - Fix animation bug - #4615 */ 385 | .ui-accordion { width: 100%; } 386 | .ui-accordion .ui-accordion-header { cursor: pointer; position: relative; margin-top: 1px; zoom: 1; } 387 | .ui-accordion .ui-accordion-header, .ui-accordion .ui-accordion-content { -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } 388 | .ui-accordion .ui-accordion-li-fix { display: inline; } 389 | .ui-accordion .ui-accordion-header-active { border-bottom: 0 !important; } 390 | .ui-accordion .ui-accordion-header a { display: block; font-size: 12px; font-weight: bold; padding: .5em .5em .5em .7em; } 391 | .ui-accordion-icons .ui-accordion-header a { padding-left: 2.2em; } 392 | .ui-accordion .ui-accordion-header .ui-icon { position: absolute; left: .5em; top: 50%; margin-top: -8px; } 393 | .ui-accordion .ui-accordion-content { padding: 1em 2.2em; border-top: 0; margin-top: -2px; position: relative; top: 1px; margin-bottom: 2px; overflow: auto; display: none; zoom: 1; } 394 | .ui-accordion .ui-accordion-content-active { display: block; }/* 395 | * jQuery UI Autocomplete 1.8.7 396 | * 397 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 398 | * Dual licensed under the MIT or GPL Version 2 licenses. 399 | * http://jquery.org/license 400 | * 401 | * http://docs.jquery.com/UI/Autocomplete#theming 402 | */ 403 | .ui-autocomplete { 404 | position: absolute; cursor: default; z-index: 3 !important; 405 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 406 | -moz-box-shadow: 0 1px 5px rgba(0,0,0,0.3); 407 | -webkit-box-shadow: 0 1px 5px rgba(0,0,0,0.3); 408 | } 409 | 410 | /* workarounds */ 411 | * html .ui-autocomplete { width:1px; } /* without this, the menu expands to 100% in IE6 */ 412 | 413 | /* 414 | * jQuery UI Menu 1.8.7 415 | * 416 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 417 | * Dual licensed under the MIT or GPL Version 2 licenses. 418 | * http://jquery.org/license 419 | * 420 | * http://docs.jquery.com/UI/Menu#theming 421 | */ 422 | .ui-menu { 423 | list-style:none; 424 | padding: 2px; 425 | margin: 0; 426 | display:block; 427 | float: left; 428 | } 429 | .ui-menu .ui-menu { 430 | margin-top: -3px; 431 | } 432 | .ui-menu .ui-menu-item { 433 | margin:0; 434 | padding: 0; 435 | zoom: 1; 436 | float: left; 437 | clear: left; 438 | width: 100%; 439 | } 440 | .ui-menu .ui-menu-item a { 441 | text-decoration:none; 442 | display:block; 443 | padding:.2em .4em; 444 | line-height:1.5; 445 | zoom:1; 446 | } 447 | .ui-menu .ui-menu-item a.ui-state-hover, 448 | .ui-menu .ui-menu-item a.ui-state-active { 449 | font-weight: normal; 450 | margin: -1px; 451 | background: #5f83b9; 452 | color: #FFFFFF; 453 | text-shadow: 0px 1px 1px #234386; 454 | border-color: #466086; 455 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 456 | } 457 | /* 458 | * jQuery UI Button 1.8.7 459 | * 460 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 461 | * Dual licensed under the MIT or GPL Version 2 licenses. 462 | * http://jquery.org/license 463 | * 464 | * http://docs.jquery.com/UI/Button#theming 465 | */ 466 | .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; -webkit-user-select: none; -moz-user-select: none; user-select: none; } /* the overflow property removes extra width in IE */ 467 | .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */ 468 | button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */ 469 | .ui-button-icons-only { width: 3.4em; } 470 | button.ui-button-icons-only { width: 3.7em; } 471 | 472 | /* button animation properties */ 473 | .ui-button { 474 | -webkit-transition: all 0.25s ease-in-out; 475 | -moz-transition: all 0.25s ease-in-out; 476 | -o-transition: all 0.25s ease-in-out; 477 | } 478 | 479 | /*states*/ 480 | .ui-button.ui-state-hover { 481 | -moz-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; 482 | -webkit-box-shadow: 0 0 8px rgba(0, 0, 0, 0.15), 0 1px 0 rgba(255,255,255,0.8) inset; 483 | } 484 | .ui-button.ui-state-focus { 485 | outline: none; 486 | color: #1c4257; border-color: #7096ab; 487 | background-image: -webkit-gradient( 488 | linear, 489 | left bottom, 490 | left top, 491 | color-stop(1, rgb(185,224,245)), 492 | color-stop(0, rgb(146,189,214)) 493 | ); 494 | background-image: -moz-linear-gradient( 495 | center top, 496 | rgb(185,224,245), 497 | rgb(146,189,214) 498 | ); 499 | -webkit-box-shadow: none; 500 | -moz-box-shadow: none; 501 | } 502 | 503 | /*button text element */ 504 | .ui-button .ui-button-text { display: block; line-height: 1.4; font-size: 14px; font-weight: bold; text-shadow: 0 1px 0 rgba(255, 255, 255, 0.6); } 505 | .ui-button-text-only .ui-button-text { padding: .4em 1em; } 506 | .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: .4em; text-indent: -9999999px; } 507 | .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 1em .4em 2.1em; } 508 | .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: .4em 2.1em .4em 1em; } 509 | .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; } 510 | /* no icon support for input elements, provide padding by default */ 511 | input.ui-button { padding: .4em 1em; } 512 | 513 | /*button icon element(s) */ 514 | .ui-button-icon-only .ui-icon, .ui-button-text-icon-primary .ui-icon, .ui-button-text-icon-secondary .ui-icon, .ui-button-text-icons .ui-icon, .ui-button-icons-only .ui-icon { position: absolute; top: 50%; margin-top: -8px; } 515 | .ui-button-icon-only .ui-icon { left: 50%; margin-left: -8px; } 516 | .ui-button-text-icon-primary .ui-button-icon-primary, .ui-button-text-icons .ui-button-icon-primary, .ui-button-icons-only .ui-button-icon-primary { left: .5em; } 517 | .ui-button-text-icon-secondary .ui-button-icon-secondary, .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 518 | .ui-button-text-icons .ui-button-icon-secondary, .ui-button-icons-only .ui-button-icon-secondary { right: .5em; } 519 | 520 | /*button sets*/ 521 | .ui-buttonset { margin-right: 7px; } 522 | .ui-buttonset .ui-button { margin-left: 0; margin-right: -.3em; } 523 | .ui-buttonset .ui-button.ui-state-active { color: #1c4257; border-color: #7096ab; } 524 | .ui-buttonset .ui-button.ui-state-active { 525 | background-image: -webkit-gradient( 526 | linear, 527 | left bottom, 528 | left top, 529 | color-stop(1, rgb(185,224,245)), 530 | color-stop(0, rgb(146,189,214)) 531 | ); 532 | background-image: -moz-linear-gradient( 533 | center top, 534 | rgb(185,224,245), 535 | rgb(146,189,214) 536 | ); 537 | -webkit-box-shadow: none; 538 | -moz-box-shadow: none; 539 | } 540 | 541 | /* workarounds */ 542 | button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */ 543 | /* 544 | * jQuery UI Dialog 1.8.7 545 | * 546 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 547 | * Dual licensed under the MIT or GPL Version 2 licenses. 548 | * http://jquery.org/license 549 | * 550 | * http://docs.jquery.com/UI/Dialog#theming 551 | */ 552 | .ui-dialog { position: absolute; padding: 0; width: 300px; overflow: hidden; } 553 | .ui-dialog { 554 | -webkit-box-shadow: 0 2px 12px rgba(0,0,0,0.6); 555 | -moz-box-shadow: 0 2px 12px rgba(0,0,0,0.6); 556 | } 557 | .ui-dialog .ui-dialog-titlebar { padding: 0.7em 1em 0.6em 1em; position: relative; border: none; border-bottom: 1px solid #979797; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } 558 | .ui-dialog .ui-dialog-title { float: left; margin: .1em 16px .2em 0; font-size: 14px; text-shadow: 0 1px 0 rgba(255,255,255,0.5); } 559 | .ui-dialog .ui-dialog-titlebar-close { position: absolute; right: .8em; top: 55%; width: 16px; margin: -10px 0 0 0; padding: 0; height: 16px; } 560 | .ui-dialog .ui-dialog-titlebar-close span { display: block; margin: 1px; background: url(images/icon_sprite.png) 0 -16px no-repeat; } 561 | .ui-dialog .ui-dialog-titlebar-close:hover span { background-position: -16px -16px; } 562 | .ui-dialog .ui-dialog-titlebar-close:hover, .ui-dialog .ui-dialog-titlebar-close:focus { padding: 0; border: 0; } 563 | .ui-dialog .ui-dialog-content { position: relative; border: 0; padding: .5em 1em; background: none; overflow: auto; zoom: 1; } 564 | .ui-dialog .ui-dialog-buttonpane { text-align: left; border-width: 1px 0 0 0; background-image: none; margin: .5em 0 0 0; padding: .3em 1em .5em .4em; } 565 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { float: right; } 566 | .ui-dialog .ui-dialog-buttonpane button { margin: .5em .4em .5em 0; cursor: pointer; } 567 | .ui-dialog .ui-resizable-se { width: 14px; height: 14px; right: 3px; bottom: 3px; } 568 | .ui-draggable .ui-dialog-titlebar { cursor: move; } 569 | /* 570 | * jQuery UI Slider 1.8.7 571 | * 572 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 573 | * Dual licensed under the MIT or GPL Version 2 licenses. 574 | * http://jquery.org/license 575 | * 576 | * http://docs.jquery.com/UI/Slider#theming 577 | */ 578 | .ui-slider { position: relative; text-align: left; background: #d7d7d7; } 579 | .ui-slider { -moz-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; -webkit-box-shadow: 0 1px 2px rgba(0,0,0,0.5) inset; } 580 | .ui-slider .ui-slider-handle { background: url(images/slider_handles.png) 0px -23px no-repeat; position: absolute; z-index: 2; width: 23px; height: 23px; cursor: default; border: none; outline: none; -moz-box-shadow: none; -webkit-box-shadow: none; } 581 | .ui-slider .ui-state-hover, .ui-slider .ui-state-active { background-position: 0 0; } 582 | .ui-slider .ui-slider-range { background: #a3cae0; position: absolute; z-index: 1; font-size: .7em; display: block; border: 0; background-position: 0 0; } 583 | .ui-slider .ui-slider-range { -moz-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; -webkit-box-shadow: 0 1px 2px rgba(17,35,45,0.6) inset; } 584 | 585 | 586 | .ui-slider-horizontal { height: 5px; } 587 | .ui-slider-horizontal .ui-slider-handle { top: -8px; margin-left: -13px; } 588 | .ui-slider-horizontal .ui-slider-range { top: 0; height: 100%; } 589 | .ui-slider-horizontal .ui-slider-range-min { left: 0; } 590 | .ui-slider-horizontal .ui-slider-range-max { right: 0; } 591 | 592 | .ui-slider-vertical { width: 5px; height: 100px; } 593 | .ui-slider-vertical .ui-slider-handle { left: -8px; margin-left: 0; margin-bottom: -13px; } 594 | .ui-slider-vertical .ui-slider-range { left: 0; width: 100%; } 595 | .ui-slider-vertical .ui-slider-range-min { bottom: 0; } 596 | .ui-slider-vertical .ui-slider-range-max { top: 0; }/* 597 | * jQuery UI Tabs 1.8.7 598 | * 599 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 600 | * Dual licensed under the MIT or GPL Version 2 licenses. 601 | * http://jquery.org/license 602 | * 603 | * http://docs.jquery.com/UI/Tabs#theming 604 | */ 605 | .ui-tabs { position: relative; zoom: 1; border: 0; } /* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 606 | .ui-tabs .ui-tabs-nav { margin: 0; padding: 0; background: transparent; border-width: 0 0 1px 0; } 607 | .ui-tabs .ui-tabs-nav { 608 | -moz-border-radius: 0; 609 | -webkit-border-radius: 0; 610 | border-radius: 0; 611 | } 612 | .ui-tabs .ui-tabs-nav li { list-style: none; float: left; position: relative; top: 1px; margin: 0 .2em 1px 0; border-bottom: 0 !important; padding: 0; white-space: nowrap; } 613 | .ui-tabs .ui-tabs-nav li a { float: left; padding: .5em 1em; text-decoration: none; font-size: 12px; font-weight: bold; text-shadow: 0 1px 0 rgba(255,255,255,0.5); } 614 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected { margin-bottom: 0; padding-bottom: 1px; background: #fff; border-color: #B6B6B6; } 615 | .ui-tabs .ui-tabs-nav li.ui-tabs-selected a, .ui-tabs .ui-tabs-nav li.ui-state-disabled a, .ui-tabs .ui-tabs-nav li.ui-state-processing a { cursor: text; outline: none; } 616 | .ui-tabs .ui-tabs-nav li a, .ui-tabs.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-selected a { cursor: pointer; } /* first selector in group seems obsolete, but required to overcome bug in Opera applying cursor: text overall if defined elsewhere... */ 617 | .ui-tabs .ui-tabs-panel { display: block; border-width: 0 1px 1px 1px; padding: 1em 1.4em; background: none; } 618 | .ui-tabs .ui-tabs-panel { 619 | -moz-border-radius: 0; 620 | -webkit-border-radius: 0; 621 | border-radius: 0; 622 | } 623 | .ui-tabs .ui-tabs-hide { display: none !important; } 624 | /* 625 | * jQuery UI Datepicker 1.8.7 626 | * 627 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 628 | * Dual licensed under the MIT or GPL Version 2 licenses. 629 | * http://jquery.org/license 630 | * 631 | * http://docs.jquery.com/UI/Datepicker#theming 632 | */ 633 | .ui-datepicker { width: 17em; padding: 0; display: none; border-color: #DDDDDD; } 634 | .ui-datepicker { 635 | -moz-box-shadow: 0 4px 8px rgba(0,0,0,0.5); 636 | -webkit-box-shadow: 0 4px 8px rgba(0,0,0,0.5); 637 | box-shadow: 0 4px 8px rgba(0,0,0,0.5); 638 | } 639 | .ui-datepicker .ui-datepicker-header { position:relative; padding:.35em 0; border: none; border-bottom: 1px solid #B6B6B6; -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; } 640 | .ui-datepicker .ui-datepicker-prev, .ui-datepicker .ui-datepicker-next { position:absolute; top: 6px; width: 1.8em; height: 1.8em; } 641 | .ui-datepicker .ui-datepicker-prev-hover, .ui-datepicker .ui-datepicker-next-hover { border: 1px none; } 642 | .ui-datepicker .ui-datepicker-prev { left:2px; } 643 | .ui-datepicker .ui-datepicker-next { right:2px; } 644 | .ui-datepicker .ui-datepicker-prev span { background-position: 0px -32px !important; } 645 | .ui-datepicker .ui-datepicker-next span { background-position: -16px -32px !important; } 646 | .ui-datepicker .ui-datepicker-prev-hover span { background-position: 0px -48px !important; } 647 | .ui-datepicker .ui-datepicker-next-hover span { background-position: -16px -48px !important; } 648 | .ui-datepicker .ui-datepicker-prev span, .ui-datepicker .ui-datepicker-next span { display: block; position: absolute; left: 50%; margin-left: -8px; top: 50%; margin-top: -8px; background: url(images/icon_sprite.png) no-repeat; } 649 | .ui-datepicker .ui-datepicker-title { margin: 0 2.3em; line-height: 1.8em; text-align: center; font-size: 12px; text-shadow: 0 1px 0 rgba(255,255,255,0.6); } 650 | .ui-datepicker .ui-datepicker-title select { font-size:1em; margin:1px 0; } 651 | .ui-datepicker select.ui-datepicker-month-year {width: 100%;} 652 | .ui-datepicker select.ui-datepicker-month, 653 | .ui-datepicker select.ui-datepicker-year { width: 49%;} 654 | .ui-datepicker table {width: 100%; font-size: .9em; border-collapse: collapse; margin:0 0 .4em; } 655 | .ui-datepicker th { padding: .7em .3em; text-align: center; font-weight: bold; border: 0; } 656 | .ui-datepicker td { border: 0; padding: 1px; } 657 | .ui-datepicker td span, .ui-datepicker td a { display: block; padding: .2em; text-align: right; text-decoration: none; } 658 | .ui-datepicker .ui-datepicker-buttonpane { background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; } 659 | .ui-datepicker .ui-datepicker-buttonpane button { float: right; margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } 660 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { float:left; } 661 | .ui-datepicker .ui-state-default { background: transparent; border-color: #FFF; } 662 | .ui-datepicker .ui-state-active { background: #5F83B9; border-color: #5F83B9; color: #FFF; font-weight: bold; text-shadow: 0 1px 1px #234386; } 663 | 664 | /* with multiple calendars */ 665 | .ui-datepicker.ui-datepicker-multi { width:auto; } 666 | .ui-datepicker-multi .ui-datepicker-group { float:left; } 667 | .ui-datepicker-multi .ui-datepicker-group table { width:95%; margin:0 auto .4em; } 668 | .ui-datepicker-multi-2 .ui-datepicker-group { width:50%; } 669 | .ui-datepicker-multi-3 .ui-datepicker-group { width:33.3%; } 670 | .ui-datepicker-multi-4 .ui-datepicker-group { width:25%; } 671 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header { border-left-width:0; } 672 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { border-left-width:0; } 673 | .ui-datepicker-multi .ui-datepicker-buttonpane { clear:left; } 674 | .ui-datepicker-row-break { clear:both; width:100%; } 675 | 676 | /* RTL support */ 677 | .ui-datepicker-rtl { direction: rtl; } 678 | .ui-datepicker-rtl .ui-datepicker-prev { right: 2px; left: auto; } 679 | .ui-datepicker-rtl .ui-datepicker-next { left: 2px; right: auto; } 680 | .ui-datepicker-rtl .ui-datepicker-prev:hover { right: 1px; left: auto; } 681 | .ui-datepicker-rtl .ui-datepicker-next:hover { left: 1px; right: auto; } 682 | .ui-datepicker-rtl .ui-datepicker-buttonpane { clear:right; } 683 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { float: left; } 684 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current { float:right; } 685 | .ui-datepicker-rtl .ui-datepicker-group { float:right; } 686 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 687 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { border-right-width:0; border-left-width:1px; } 688 | 689 | /* IE6 IFRAME FIX (taken from datepicker 1.5.3 */ 690 | .ui-datepicker-cover { 691 | display: none; /*sorry for IE5*/ 692 | display/**/: block; /*sorry for IE5*/ 693 | position: absolute; /*must have*/ 694 | z-index: -1; /*must have*/ 695 | filter: mask(); /*must have*/ 696 | top: -4px; /*must have*/ 697 | left: -4px; /*must have*/ 698 | width: 200px; /*must have*/ 699 | height: 200px; /*must have*/ 700 | }/* 701 | * jQuery UI Progressbar 1.8.7 702 | * 703 | * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) 704 | * Dual licensed under the MIT or GPL Version 2 licenses. 705 | * http://jquery.org/license 706 | * 707 | * http://docs.jquery.com/UI/Progressbar#theming 708 | */ 709 | .ui-progressbar { height: 12px; text-align: left; background: #FFF url(images/progress_bar.gif) 0 -14px repeat-x; } 710 | .ui-progressbar .ui-progressbar-value {margin: -1px; height:100%; background: url(images/progress_bar.gif) 0 0 repeat-x; } 711 | 712 | /* Extra Input Field Styling */ 713 | textarea, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]) { 714 | padding: 3px; 715 | -webkit-border-radius: 2px; 716 | -moz-border-radius: 2px; 717 | border-radius: 2px; 718 | border: 1px solid #cecece; 719 | outline: none; 720 | -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.1) inset, 0 1px 0 rgba(255,255,255,0.2); 721 | -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.1) inset, 0 1px 0 rgba(255,255,255,0.2); 722 | box-shadow: 0 1px 3px rgba(0,0,0,0.1) inset, 0 1px 0 rgba(255,255,255,0.2); 723 | -webkit-transition: all 0.25s ease-in-out; 724 | -moz-transition: all 0.25 ease-in-out; 725 | -o-transition: all 0.25s ease-in-out; 726 | } 727 | textarea:hover, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):hover { 728 | border: 1px solid #bdbdbd; 729 | -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2); 730 | -moz-box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2); 731 | box-shadow: 0 1px 3px rgba(0,0,0,0.2) inset, 0 1px 0 rgba(255,255,255,0.2); 732 | } 733 | textarea:focus, input:not([type="checkbox"]):not([type="radio"]):not([type="file"]):focus { 734 | border: 1px solid #95bdd4; 735 | -webkit-box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2); 736 | -moz-box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2); 737 | box-shadow: 0 2px 3px rgba(161,202,226,0.5) inset, 0 1px 0 rgba(255,255,255,0.2); 738 | } --------------------------------------------------------------------------------